Modifying Structs In List vs Array

David Klempfner
Level Up Coding
Published in
4 min readDec 13, 2019

--

Photo by Mohamad Mahdi Abbasi on Unsplash

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 stack
  • ldstr “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 property SomeProp 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.

--

--

I’m a software developer who is passionate about learning how things work behind the scenes.