Mutating value types – Part 2

In the previous blog post, we found that mutating a struct inside a class works if the struct is declared as a field, but doesn’t work if it is declared as a property.

The reason is fairly obvious – if struct fields also returned a copy, then there wouldn’t be any way of mutating the instance at all, even from within the declaring class.

  1: struct S
  2: {
  3:     public int X;
  4: }
  6: class C
  7: {
  8:     public S S;
 10:     void SetX()
 11:     {
 12:         this.S.X = 1; // Won't work if this.S returned a copy
 13:     }
 14: }

S would essentially act like a readonly field, except that you can’t change it even from within the constructor.

With that out of the way, let’s see how field access works under the covers – here’s the generated IL.

  1: .method public hidebysig static void Main() cil managed
  2: {
  3:     .entrypoint
  4:     .maxstack 2
  5:     .locals init (
  6:         [0] class C c)
  7:     L_0000: nop 
  8:     L_0001: newobj instance void C::.ctor()
  9:     L_0006: stloc.0 
 10:     L_0007: ldloc.0 
 11:     L_0008: ldflda valuetype S C::S
 12:     L_000d: ldc.i4.1 
 13:     L_000e: stfld int32 S::X
 14:     L_0013: ret 
 15: }

The key here is the ldflda instruction – MSDN says it “finds the address of a field in the object whose reference is currently on the evaluation stack”. In contrast, here’s how property access is compiled.

  1: .method public hidebysig static void Main() cil managed
  2: {
  3:     .entrypoint
  4:     .maxstack 1
  5:     .locals init (
  6:         [0] class C c,
  7:         [1] int32 val)
  8:     L_0000: nop 
  9:     L_0001: newobj instance void C::.ctor()
 10:     L_0006: stloc.0 
 11:     L_0007: ldloc.0 
 12:     L_0008: callvirt instance valuetype S C::get_S()
 13:     L_000d: ldfld int32 S::X
 14:     L_0012: stloc.1 
 15:     L_0013: ret 
 16: }

C::get_S() obviously returns a copy of S, and that’s the difference – ldflda loads the address of the instance instead. 

To summarize, for a struct declared in a class, the compiler disallows mutating it if its exposed through an instance property, but allows it if it is a non-readonly field.

How does the compiler detect mutation though? What happens if I call a method on the struct, rather than change a field inside it? More about it in the next post.

Leave a Reply

Your email address will not be published. Required fields are marked *