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: }
  5: 
  6: class C
  7: {
  8:     public S S;
  9: 
 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.

Mutating value types – Part 1

Take a look at the following short snippet of code.

  1: using System;
  2: 
  3: struct S
  4: {
  5:     public int X;
  6: }
  7: 
  8: class C
  9: {
 10:     /* More code here */
 11: }
 12: 
 13: class Test
 14: {
 15:     public static void Main()
 16:     {
 17:         C c = new C();
 18:         c.S.X = 1;
 19:     }
 20: }


Without knowing the type definition of C, can you tell whether the code will compile, much less work?



Turns out that you can’t. It depends on whether the S in c.S is defined as a field or a property.


1.

class C
{
    public S S;
}

2.

class C
{
    public S S { get; private set; }
}


The first type definition will compile, the second won’t (error CS1612: Cannot modify the return value of ‘C.S’ because it is not a variable)



Can you guess why?



Let’s assume the compiler does allow the second type definition to compile. Would you expect the value of X in the instance of S inside C to change? That is, what would be the output of



public static void Main()
{
    C c = new C();
    c.S.X = 1;
    Console.WriteLine(c.S.X);
}


If you’re expecting it to be 1, then you have just broken the value semantics of a struct, S might as well have been defined as a class. The above code is logically identical to



public static void Main()
{
    C c = new C();
    S temp = c.S;
    temp.X = 1;
    Console.WriteLine(c.S.X);
}


Because S is a struct, the property getter always returns a copy (temp), and changing the copy will have no effect on the original instance. With c.S.X = 1, you can’t access the copy either, so the only effect of executing that statement will be to make the poor developer’s eyes go wide as he steps through the code in the debugger, wondering why a simple field assignment refuses to work.



So, the C# compiler is being helpful here by not allowing this kind of code to compile.



Right, so why does it compile if S is defined as a field rather than a property? We’ll see why in the next post. Meanwhile, feel free to post why you think it works.