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.