Most of the programmers who are not easy to bring together with the platform .Net are aware of the existence of significant types (value types) and reference types (reference types). And quite a few of them are well aware that in addition to the name, these types have other differences, such as the location of objects of these types in memory, as well as in semantics.
Regarding the first distinction (which is worth mentioning at least for the sake of completeness), it is that instances of reference types are always located on the managed heap, while instances of significant types are by default on the stack, but can migrate heaps due to packaging, being members of reference types, as well as using them in cunning exotic constructions of the C # language, such as closures (*).
Although this difference is very important and it is thanks to it that significant types exist and are used, this pair of types has one more, no less important semantic difference. Meaningful types, as the name suggests, are values ​​that are
copied each time a function is passed to or returned from a function . And since when copying, as, again, the name suggests, it is not the original version that is transferred and returned, but a copy, all attempts at change will lead to a change in the copy, not the original instance.
')
In theory, the last statement seems so simple and obvious that it seems unworthy of attention, but in C # there are a number of moments when copying is so implicit that it leads to copying the wrong copy of the developer that he thinks ) in easy confusion.
Let's look at some of these examples.
1. Changeable type as an object property
Let's start with a relatively simple example in which copying is quite explicit. Suppose we have some mutable significant type (which, by the way, will be useful not only for this, but for all the following examples) 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 Mutable Mutable { get ; private set ; }
}
* This source code was highlighted with Source Code Highlighter .
So far, seemingly, nothing interesting, 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, since the second line (
a.Mutable.Y ++;) is incorrect from the point of view of the C # language. Since the value of the
Mutable structure is copied when returning from the property of the same name, the compiler understands at the compilation stage that there will be nothing good in changing the temporary object, which is eloquent in the error message: “
error CS1612: Cannot modify the return value of ' System.Collections.Generic.IList <MutableValueTypes.Mutable> .this [int] 'because it is not a variable ”. To everyone who is more or less familiar with the C ++ language, this behavior will be quite understandable, since in this line of code we are trying to do nothing more than change a value that is not 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 cannot call the ++ operator of the Y property in the previous code snippet, we can easily call the
IncrementX method of the
X property:
Console .WriteLine( " Mutable.X: {0}" , a.Mutable.X);
a.Mutable.IncrementX();
Console .WriteLine( "Mutable.X 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 notice the error with the naked eye. Each time when the
Mutable class property is accessed, a new copy is created, for which the
IncrementX method is called, but since the copy change has nothing to do with the change of the original object, the output to the console, when executing the previous code fragment, will be appropriate:
Baseline Mutable.X: 5
Mutable.X after calling IncrementX (): 5
"Hmm ... nothing supernatural," say you and you will be right ... until we consider other, more interesting cases.
2. Changeable significant types and readonly modifier
Let's look at class
B , which
readonly contains our mutable mutable structure as a
readonly field:
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, the only drawback of which is the use of an open field. But since the openness of this field is due to a simple example and convenience, and not design errors, then you should not pay attention to this trifle. Instead, pay attention to a simple example of using this class and the results obtained.
B b = new B();
Console .WriteLine( " MX: {0}" , bMX);
bMIncrementX();
bMIncrementX();
bMIncrementX();
Console .WriteLine( "MX IncrementX: {0}" , bMX);
* This source code was highlighted with Source Code Highlighter .
So what will be the result? eight? (I recall that the initial value of the property
X is 5, and 5 + 3, as you know, is 8; 7 it would probably be better, but, alas, it turns out as much as 8) Or maybe -8? Joke.
It seems that
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, by the way, too) will disagree with us and, as a result of the execution of this code,
MX will still be equal to 5:
Initial MX value: 5
MX after three calls to IncrementX (): 5
The point here is that
according to the specification, when accessing a read-only field outside the constructor, a temporary variable is generated, for which the IncrementX method is called . In fact, the previous code snippet is written by the compiler in the following way:
Console .WriteLine( " MX: {0}" , bMX);
Mutable tmp1 = bM;
tmp1.IncrementX();
Mutable tmp2 = bM;
tmp2.IncrementX();
Mutable tmp3 = bM;
tmp3.IncrementX();
Console .WriteLine( "MX IncrementX: {0}" , bMX);
* This source code was highlighted with Source Code Highlighter .
(Yes, if you remove the
readonly modifier, you will get the expected result; after three calls to the
IncrementX method, the value of the
X property of the variable
M will be equal to 8.)
3. Arrays and lists
The next, but obviously not the last, moment of unobvious behavior of mutable significant types is their use in arrays and lists. So, let's put one element of a changeable significant type into a collection, for example into a
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 indexer of the list is a common property, its behavior does not differ from what is described in the first section: every time referring to a list item, we will receive not a source item, but a copy of it.
lm[0].Y++; //
lm[0].IncrementX(); //
* This source code was highlighted with Source Code Highlighter .
Now let's try doing the same array operation:
Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) };
Console .WriteLine( " X: {0}, Y: {1}" , am[0].X, am[0].Y);
am[0].Y++;
am[0].IncrementX();
Console .WriteLine( " 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 in a similar way, returning a copy of the element, which then changes in our code. And since the C # language does not support this feature, such as returning “managed pointers” (managed pointers) from a function, there seems to be no other options. After all, all we can do is create synonyms for our variable (alias) and pass it to another function using the keywords
ref or
out , but we cannot write a function that returns a reference to one of the object fields.
But although the C # language does not support the return of managed references in the general case, there is a special optimization in the form of a special IL-code instruction that allows you to get not just a copy of the array element, but a link to it (for the curious, this instruction is called
ldelema ). Thanks to this feature, the previous fragment is not only completely correct (including the string 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 object of the array.
Initial values ​​X: 5, Y: 5
New values ​​X: 6, Y: 6
However, if the array considered above is brought to one of its interfaces, such as
IList <T> , then all street magic in the form of generating specific 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++; //
lst[0].IncrementX(); //
* This source code was highlighted with Source Code Highlighter .
4. And why do I need all this?
The question is reasonable, especially if you remember how often you create your own meaningful types and even more so how often you make them changeable. But the benefits of this knowledge is. First, you and I are not the only programmers in the world, as it is not difficult to guess, there are many other Gavriks who rivet the code with terrible force and create their own variable structures. And even if you personally do not have such Gavriks in your team, they are in other teams, for example, in the .Net Framework development team. Yes, as part of the .Net Framework, there are a sufficient number of mutable significant types, the careless use of which can lead to costly surprises (**).
A classic example of a variable value type is the
Point structure, as well as enumerators, for example
ListEnumerator . And if in the first case it is very difficult to cut off your leg, in the second case - be healthy:
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 in
LINQPad or in the
Main method and run.)
Conclusion
It is categorical to say that mutable significant types are a complete evil, just as wrong, as well as talking about a comprehensive evil of the
goto operator. It is known that the use of the
goto operator by a programmer directly in a large industrial system can lead to code that is difficult to understand and maintain, hidden errors and a headache when searching for errors. For the same reason, you need to beware of changeable significant types: if you know how to prepare them, their careful use can be a good performance optimization. But this efficiency may well come back to you later, when your neighbor, who has not yet learned the C # language specification for a tooth, and still does not know that using the using construct with significant types results in copy cleaning (***), will take care of it.
The use of significant types is already an optimization, so you definitely need to prove that their use is worth it and you will get a performance boost. The use of changeable significant types is optimization in a square (after all, you save on a copy when they change), so you should first make your significant types changeable not
n times, but
n times in a square.
-----------------------------
(*) A closure is not such a terrible beast, as it may seem from an intricate name. And if suddenly, for some reason, you are not confident in your knowledge about this, then this is just an excellent reason to fix it:
“C # closures” .
(**) What is most interesting, changeable significant types are far from the only dubious solution, the manifestation of which can easily be found in the .Net Framework. Another equally dubious design decision is the behavior of
virtual events (which I wrote earlier), and with all their ambiguous behavior, they are also present in the .Net Framework (for example,
PropertyChanged and
CollectionChanged events of the
ObservableCollection class are virtual)
(***) This is a subtle hint at one of Eric Lippert’s articles (which considers mutable significant types as the greatest universal evil), in which he shows “not quite obvious” behavior when using mutable significant types that implement the
IDisposable interface:
To box or not to box, that is a question .