Home .NET About the harm of changeable value types

About the harm of changeable value types

by admin

The majority of programmers, who have had a hard fate to getacquainted with platform.Net, know about the existence of value types and reference types.And most of them know that these types have different names and other differences, such as location of objects of these types in memory and semantics.
Regarding the first difference (worth mentioning at least for completeness’s sake), it is that instances of referential types are always in the managed heap, while instances of significant types are in the stack by default but can migrate to the managed heap because of packing, being members of referential types and also when used in clever exotic C# constructions, like closures (*).
Although thisdistinction is very important, and it is the reason why meaningful types exist and are used, this pair of types has another, no less important semantic distinction. Meaningful types, as the name suggests, are values that is copied each time it is passed to or returned from a function And since copying, as the name again suggests, does not pass and return the original version, but a copy, all attempts to change will result in changes to the copy, not to the original instance.
In theory, the last statement seems so simple and obvious that it seems unworthy of attention, but there are some points in C# where copying is so implicit that it leads to copying a completely different instance than the developer thinks it should, leaving him (the developer) mildly confused.
Let’s look at some of these examples.

1. Variable meaningful type as an object property

Let’s start with a relatively simple example where the copying happens quite explicitly. Suppose we have some variable meaningful type (which, by the way, is useful not only for this example, but for all the following ones) called Mutable and some class A which contains a property of the specified type :

struct Mutable
{
public Mutable( int x, int y)
: this ()
{
X= x;
Y = y;
}
public void IncrementX() {X++;}
public int X{ get ; private set ;}
public int Y { get ; set ; }
}
class A
{
public A() { Mutable= new Mutable(x:5, y:5); }
public MutableMutable{ get ; private set ; }
}
* This source code was highlighted with Source Code Highlighter

Nothing interesting so far, it seems, but let’s look at the following example :

A a = new A();
a.Mutable.Y++;
* This source code was highlighted with Source Code Highlighter

The most interesting thing is that this code will not compile at all, because the second line ( a.Mutable.Y++;) is incorrect from the C# point of view. Since the value of the structure Mutable is copied when returning from the property with the same name, the compiler knows already at the stage of compilation that nothing good will come of changing the temporary object, and the error message says about it eloquently: " error CS1612: Cannot modify the return value of ‘System.Collections.Generic.IList<MutableValueTypes.Mutable> .this[int]’ because it is not a variable ". Anyone who is more or less familiar with the C++ language will find this behavior quite understandable, because in this line of code we are trying to do nothing more than change a value that is not an l-value.
Although the compiler understands the semantics of the ++ operator, in general it has no idea what a particular function does with the current object, in particular whether it changes it or not. And although we can’t call the ++ operator on property Y in the previous snippet, we can safely call the IncrementX properties X :

Console WriteLine( "The original value of Mutable.X: {0}" , a.Mutable.X);
a.Mutable.IncrementX();
Console WriteLine( "Mutable.X after calling IncrementX():{0}" , a.Mutable.X);
* This source code was highlighted with Source Code Highlighter

Although the previous code behaves incorrectly, it is not always easy to spot the error with the naked eye. Every time you access the Mutable class, a newcopy is created, and the IncrementX method, but since changing a copy has nothing to do with changing the original object, the output to the console when running the previous code fragment will be the same as :
Initial value Mutable.X: 5
Mutable.X after calling IncrementX(): 5

"Hmmm…nothing supernatural, " you say, and you’d be right…until we look at other, more interesting cases.

2. Modifiable significant types and modifier readonly

Let’s look at the class B , which as a readonly field contains our changeable structure Mutable :

class B
{
public readonly Mutable M= new Mutable(x: 5, y: 5);
}
* This source code was highlighted with Source Code Highlighter

Again, this is not rocket science, but the simplest class whose only drawback is the use of an open field. But since the open field is due to simple example and convenience rather than design flaws, it is not worth paying attention to this triviality. Instead, it’s worth paying attention to a simple example of using this class and the results we get.

B b = new B();
Console WriteLine( "The original value of M.X: {0}" , b.M.X);
b.M.IncrementX();
b.M.IncrementX();
b.M.IncrementX();
Console WriteLine( "M.X After three IncrementX calls: {0}" , b.M.X);
* This source code was highlighted with Source Code Highlighter

So, what will the result be? 8? (Recall that the original value of the property X is 5, while 5 + 3 is known to be 8; 7 would probably be better, but alas, it comes out to 8.) Or maybe -8? Just kidding.
Sort of M – is not a property that will be copied every time it is returned, so answer 8 seems quite logical. However, the compiler (and the C# language specification, too, by the way) will disagree with us and, as a result of executing this code, M.X will still be equal to 5:
Initial M.X value: 5
M.X after three calls to IncrementX(): 5

The whole point here is that according to the specification, accessing a read-only field outside the constructor generates a temporary variable for which the IncrementX In fact, the previous code fragment is rewritten by the compiler in this way :

Console WriteLine( "The original value of M.X: {0}" , b.M.X);
Mutable tmp1 = b.M;
tmp1.IncrementX();
Mutable tmp2 = b.M;
tmp2.IncrementX();
Mutable tmp3 = b.M;
tmp3.IncrementX();
Console WriteLine( "M.X after three IncrementX calls: {0}" , b.M.X);
* This source code was highlighted with Source Code Highlighter

(Yes, if you remove the modifier readonly you will get the expected result; after three calls of the IncrementX property value X of the variable M will equal 8.)

3. Arrays and lists

Another, but obviously not the last, point of non-obvious behavior of variable significant types is their use in arrays and lists. So, let’s put one element of a changeable significant type into a collection, e.g., a list List<T>

List <Mutable> lm = new List <Mutable> { new Mutable(x:5, y: 5) };
* This source code was highlighted with Source Code Highlighter

Since the list indexer is a normal property, its behavior does not differ from the one described inthe first section: each time we access anitem in the list, we get a copy, not the original item.

lm[0].Y++; // Compilation error
lm[0].IncrementX(); // leads to a time variable change
* This source code was highlighted with Source Code Highlighter

Now let’s try to do the same operation with the array :

Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) };
Console WriteLine( "Initial values X: {0}, Y: {1}" , am[0].X, am[0].Y);
am[0].Y++;
am[0].IncrementX();
Console WriteLine( "New values X: {0}, Y: {1}" , am[0].X, am[0].Y);
* This source code was highlighted with Source Code Highlighter

In this case, most developers will assume that the array indexer behaves the same way, returning a copy of the element, which is then modified in our code. And since C# language does not support such feature as return of "managed pointers" from function, there seems to be no other option. After all, all we can do is create aliases for our variable and pass it into another function with ref or out but we cannot write a function that returns a reference to one of the object fields.
But although C# does not support the return of managed references in general, there is a special optimization in the form of a special IL-code instruction that allows you to get not just a copy of an array element, but a reference to it (for the curious, this instruction is called ldelema ). Thanks to this feature, the previous fragment is not only completely correct (including the line am[0].Y++;), but also allows you to change the elements of the array directly, not their copies. And if you run the previous code snippet, you will see that it compiles, runs, and directly modifies the null array object.
Initial values X: 5, Y: 5
New values X:6, Y:6

However, if the array in question above is brought to one of its interfaces, such as IList<T> then all the street magic in the form of generating special IL instructions will be left out, and we will get the behavior described at the beginning of this section.

Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) };
IList<Mutable> lst = am;
lst[0].Y++; //compilation error
lst[0].IncrementX(); // change the time variable
* This source code was highlighted with Source Code Highlighter

4. What’s in it for me?

It’s a reasonable question, especially if you remember how often you create your own meaningful types, and certainly how often you make them modifiable. But there are benefits to this knowledge. Firstly, you and I are not the only programmers in the world, as it’s not difficult to guess, there are many other "geeks" who rivet code with terrible force and create their own changeable structures. And even if there are no such "geeks" on your team personally, there are on other teams, such as the .Net Framework team. Yes, the .Net Framework has a fair number of modifiable significant types, the careless use of which can lead to costly surprises (**).
A classic example of a modifiable meaningful type is the structure Point as well as enumerators, e.g. ListEnumerator And if in the first case it is very difficult to saw off your own leg, in the second case you can do it:

var x = new {Items = new List < int > { 1, 2, 3 }.GetEnumerator() };
while (x.Items.MoveNext())
{
Console WriteLine(x.Items.Current);
}
* This source code was highlighted with Source Code Highlighter

(Copy this code into LINQPad or in the method Main and run.)

Conclusion

It is just as wrong to say categorically that changeable significant types are completely evil as it is to say that the operator is completely evil goto It is known that the use of the operator goto by a programmer directly in a large industrial system can lead to code that is difficult to understand and maintain, to hidden errors, and to headaches in searching for errors. For the same reason, you should also beware of modifiable significant types: if you know how to prepare them, careful use of them can be a nice performance optimization. But this efficiency might come back to you later, when your neighbor, who has not yet learned C# specification by heart and still does not know that usingthe using with meaningful types causes the copy to be cleared (***).
Using meaningful types is already an optimization, so you need to prove that using them is worth it and you’ll get better performance. But using changeable significant types is squared optimization (you save on the copy when you change them), so before you make your significant types changeable, you should think about n times, but n times squared.
—————————–
(*) Closure is not such a terrible beast as the intricate name might suggest. And if, suddenly, for some reason you are unsure of your knowledge on this subject, this is just the perfect excuse to fix it : "Closures in C#"
(**) Most interestingly, changeable meaningful types are not the only questionable solution, a manifestation of which can easily be found within the .Net Framework. Another equally questionable design solution is the behavior of virtual events (which I wrote about earlier), and for all their ambiguous behavior, they are also present in the .Net Framework (e.g. PropertyChanged and CollectionChanged of the ObservableCollection are virtual)
(***) This is a subtle allusion to one of Eric Lippert’s articles (who considers changeable significant types the greatest universal evil), in which he shows the "not entirely obvious" behavior when using changeable significant types that implement the interface IDisposable : To box or not to box, that is a question

You may also like