This article is part of the third annual C# Advent event, huge thanks to Matthew D. Groves for organizing it!
For my C# advent entry, I have prepared a little riddle for C# developers. Suppose we have the following code:
ValueHolder.SetValue method is implemented as follows:
ValueHolder.Value property is a simple auto-property with private setter:
SetValue are the only two members of
ValueHolder, and there is no external code running that would influence the execution in any way.
Intuitively, the program’s output should be
True. What is the actual output, though?
Can you explain the reason why the program output
False? When you have the answer ready or want to check out the solution, scroll down, please!
Ready to see the solution?
Although it might seem both
int snippets should behave the same, there is one crucial detail about the
ValueHolder type I haven’t revealed –
ValueHolder is a
It might seem this doesn’t make much of a difference, but in fact, it makes a world of difference!
One of the basic facts of
structs in C# are value types, and when you assign a struct to a new variable, a new instance is created, with all members copied. For example:
This snippet outputs:
So what happens in our original sample? In the case of the array, we get our expected output of 5:
List<int>, the result is plain 0:
To understand why the same “indexing” operation behaves differently, let’s see the generated IL (intermediate language) code. The following line of code:
Now let’s compare this to the compiled IL for:
Which looks like this:
When we look at the array’s IL output, we can see that the indexer gives us a managed pointer to the existing instance of the
ValueHolder struct and then calls the
SetValue method directly on this instance. So, in our example code, the array version actually modified the property value of the original instance within the array.
The situation is different with a list.
List<T> is a class and as such it does not have a “built-in” indexing capability. Instead, it uses the C# indexer, you can easily implement yourself for your classes. And indexer is just syntactic sugar, which generates a pair of methods –
get_Item for value retrieval and
set_Item for setting value at an index. This is the crux of our problem – because the
get_Item method returns the value at a given index, it actually creates a new copy of the struct. The code then modifies the property value on this new copy. The copy is then immediately thrown away and the list still contains the original struct instance, which has not been touched.
It should be noted the compiler does its best to help you avoid problems like this. For instance, if we tried to assign the property value directly, it would not allow that. A method call is possible, as the compiler can’t tell the call has side-effects, which affect the internal state of the instance.
The main takeaway of this riddle is that we should strive to keep our C# structs immutable. This way we always know the inner state of an existing instance is unchangeable and we clearly see when new instances are created. Instead of the
SetValue method in our example, we could have a
WithValue method, which creates and returns a new instance with the given value. The users (developers) can then clearly see the API does not modify the original instance and they must use the returned value instead.
The example source code for this article is available on my GitHub.