This information will be useful for those who are just learning C#, trying to understand the theory in practice, without reading books beforehand. This is a postscript rather than a detailed description and analysis of the topic. So, anyone interested in how structures behave in memory when working with collections in C# is welcome.
Reading Richter in the evenings, I decided to go deeper into every detail, having a great desire to understand the ideology of .Net in greater depth. Flipping through the pages of the book, I came to the conclusion that a lot of things that are designed and written every day are constantly violating some of the basics of .Net.
Let’s take structures and collections as examples. I won’t remind you that structures are value types and behave differently from reference types.
Example 1. Using old ArrayList.
System.Int32 myInt = 7;ArrayList al = new ArrayList();al.Add(myInt);
The expected result will be two different elements in the stack of type System.Int32. And one of them will already be of reference type. If we open the IL representation of this code, we will see a fair confirmation of :
IL_0002: stloc.0 // myIntIL_0003: newobj System.Collections.ArrayList..ctorIL_0008: stloc.1 // alIL_0009: ldloc.1 // alIL_000A: ldloc.0 // myIntIL_000B: box System.Int32IL_0010: callvirt System.Collections.ArrayList.Add
The box(boxing) operation creates an object in a heap of type Int32. When you add an element of type structures, each time its counterpart will be created in the heap, and the reference will go to the collection. This action is helped by generic-collections over time. Let’s look at an example and its IL representation :
Example 2. Using generalizations for collections :
System.Int32 myInt = 7;List<int> l = new List<int> ();l.Add(myInt);
This code is converted to :
IL_0002: stloc.0 // myIntIL_0003: newobj System.Collections.Generic.List..ctorIL_0008: stloc.1 // lIL_0009: ldloc.1 // lIL_000A: ldloc.0 // myIntIL_000B: callvirt System.Collections.Generic.List.Add
These kinds of collections are more advanced and can handle value-types more intelligently without using boxunbox operations. This greatly reduces memory consumption, improves performance, and reduces garbage collection.
However, after experimenting with collections for a while in more detail, I came across an unexpected point. Sometimes there is a need to write something in a style like :
IList l = new List<T> ();
The code is simple, but the result for the structures was ambiguous.
If we look at the IL representation of such code :
System.Int32 myInt = 7;IList l = new List<int> ();l.Add(myInt);
Then we get the following :
IL_0002: stloc.0 // myIntIL_0003: newobj System.Collections.Generic.List..ctorIL_0008: stloc.1 // lIL_0009: ldloc.1 // lIL_000A: ldloc.0 // myIntIL_000B: box System.Int32IL_0010: callvirt System.Collections.IList.Add
We get to the packing block again. Again, we get the object in the heap as a copy of our myInt value. Apparently, the developers have implemented collections in such a way that, indeed, explicitly specifying a generalization type for a variable affects the further behavior of the collection object, especially for meaningful types.
This was the first conclusion I tried to use to explain what was going on. In the end, the explanation was found a little later from the author himself. Any interface variable must always be placed in a heap, which is what made the extra packing operation appear.
The conclusion is simple. Theory first, and then practice, although usually things happen differently.