Modifying Structs In List vs Array
In the following code, I’ve set up a simple array and a generic list, both containing an instance of SomeStruct
:
The error
It’s strange to see the compiler complain about modifying the List when you can modify the array in almost exactly the same way, so why aren’t we allowed to do this?
Array vs List
To understand why this isn’t allowed, let’s have a look at what happens behind the scenes when you access an Array and a List using an index.
Array
Here’s the CIL code that accesses the array at index 0, and then sets the property to “Abcd”
:
Have a look at block 3 “Access and edit an element”. Here’s a brief explanation of what each line does:
ldc.i4.0
— Pushes the integer value of 0 onto the stack as an int32.ldelema ValueTypeTest.SomeStruct
— Loads the address of the array element at the index specified by the int currently ontop of the stack (ie. 0), onto the top of the stackldstr “Abcd”
— Pushes a new object reference to“Abcd”
on the stack.call instance void ValueTypeTest.SomeStruct::set_SomeProp(string)
— Calls the set property method for the propertySomeProp
passing in the string currently ontop of the stack (ie.“Abcd”
).
This is what the heap/stack looks like. The SomeProp
property is set for the SomeStruct
instance located at memory 0x10 with the string “Abcd”
located at 0x20 (nb the memory addresses are arbitrary).
As you can see, the property is set for the actual SomeStruct
instance (not a copy), located in the array in memory.
Now let’s compare this to a List.
List
Similar to the array, when you call someStructsList[0].SomeProp = “Abcd”;
it’s actually doing two things, accessing the SomeStruct
instance, and then setting the property.
Since we can’t compile someStructsList[0].SomeProp = “Abcd”
, let’s break it up into the following which does compile, and let’s see what it’s doing behind the scenes:
Here’s the CIL:
We’re only really interested in the code in the orange rectangle.
ldc.i4.0
— this loads the Int32
value 0 onto the stack.
callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1[valuetype ValueTypeTest.SomeStruct]::get_Item(int32)
— this calls the indexer method.
Unlike array access, when you access an element in a List, you’re actually using an indexer, which is a method that takes an Int32
, and returns a copy of the element located at that index in the List’s internal array.
This is the critical point, you are getting a copy of the element, not the actual element.
You have probably learnt this concept before, that when a method returns an instance of a value type, you’re getting a copy of that instance, not the actual instance (unless you use ref/out).
Conclusion
This is why the initial line of code does not compile, because you’re setting a property on a copy of the SomeStruct
instance. Because this copy is not stored anywhere, you’re just setting a property on a copy that is about to be discarded, which is probably not what you want.
If you break it up into two steps by using a local variable, the compiler does not complain, because at least you’re setting a property on a copy of the instance that you now have access to in a local variable.