Search code examples
c#.netiteratorthisvalue-type

Why can iterators in structs modify this?


I discovered that iterator methods in value types are allowed to modify this.
However, due to limitations in the CLR, the modifications are not seen by the calling method. (this is passed by value)

Therefore, identical code in an iterator and a non-iterator produce different results:

static void Main() {
    Mutable m1 = new Mutable();
    m1.MutateWrong().ToArray();     //Force the iterator to execute
    Console.WriteLine("After MutateWrong(): " + m1.Value);

    Console.WriteLine();

    Mutable m2 = new Mutable();
    m2.MutateRight();
    Console.WriteLine("After MutateRight(): " + m2.Value);
}

struct Mutable {
    public int Value;

    public IEnumerable<int> MutateWrong() {
        Value = 7;
        Console.WriteLine("Inside MutateWrong(): " + Value);
        yield break;
    }
    public IEnumerable<int> MutateRight() {
        Value = 7;
        Console.WriteLine("Inside MutateRight(): " + Value);
        return new int[0];
    }
}

Output:

Inside MutateWrong(): 7
After MutateWrong(): 0

Inside MutateRight(): 7
After MutateRight(): 7

Why isn't it a compiler error (or at least warning) to mutate a struct in an iterator?
This behavior is a subtle trap which is not easily understood.

Anonymous methods, which share the same limitation, cannot use this at all.

Note: mutable structs are evil; this should never come up in practice.


Solution

  • In order to justify a warning, it should be in a situation where the programmer is likely to get unexpected results. According to Eric Lippert, "we try to reserve warnings for only those situations where we can say with almost certainty that the code is broken, misleading or useless." Here is an instance where the warning would be misleading.

    Let's say you have this perfectly valid – if not terribly useful – object:

    struct Number
    {
        int value;
        public Number(int value) { this.value = value; }
        public int Value { get { return value; } }
        // iterator that mutates "this"
        public IEnumerable<int> UpTo(int max)
        {
            for (; value <= max; value++)
                yield return value;
        }
    }
    

    And you have this loop:

    var num = new Number(1);
    foreach (var x in num.UpTo(4))
        Console.WriteLine(num.Value);
    

    You'd expect this loop to print 1,1,1,1, not 1,2,3,4, right? So the class works exactly as you expect. This is an instance where the warning would be unjustified.

    Since this is clearly not a situation where the code is broken, misleading, or useless, how would you propose that the compiler generate an error or warning?