I was fascinated by this question from Stack Overflow after watching Jon Skeet and Rob Conery discussing it on TekPub TV. The main takeaway is this: mutable structs are evil. I rewrote Jon Skeet’s example as an MSpec (Machine.Specifications) unit test just for fun, just to add my own flavour.
1:using System.Collections.Generic;
2:using Machine.Specifications;
3:
4:internalstruct MutableStruct
5: {
6:publicint Value { get; set; }
7:
8:publicvoid AssignValue(int newValue)
9: {
10: Value = newValue;
11: }
12: }
13:
14: [Subject("Mutable struct in foreach iterator")]
15:internalclass SO13610559
16: {
17:static List<MutableStruct> list;
18:
19: Establish context = () => list = new List<MutableStruct>
20: {
21:new MutableStruct {Value = 10}
22: };
23:
24: Because of = () =>
25: {
26:foreach (MutableStruct item in list)
27: {
28: item.AssignValue(30);
29:// item.Value = 30;
30: }
31: };
32:
33: It should_equal_30 = () => list[0].Value.ShouldEqual(30);
34: }
When the above specification is run, the output is:
The key to understanding the problem is to uncomment line 29 and see what the compiler thinks of that. When you directly assign to a member of item, the compiler says no, because it is read-only (foreach iterators are read-only by definition). Therefore, each time item is accessed, the compiler takes a fresh copy of it from the original source (you can verify this by decompiling the code with a tool such as JetBrains’ dotPeek, or ildasm). When item is changed as a side effect of calling a method, the compiler doesn’t detect that we’re writing to a read-only thing, so it just lets it happen, then silently discards the results, because we were working on a copy all along, not the original.
Interestingly, in line 4, changing struct to class makes the test pass. In that situation, we’re dealing with a reference type and it is the reference that's readonly, not the referenced object. When the compiler repeatedly gets a clean copy of the reference, that doesn’t stop us modifying the underlying value.