Подтвердить что ты не робот

Почему итераторы в структурах меняют это?

Я обнаружил, что методам итератора в типах значений разрешено изменять this.
Однако из-за ограничений в CLR модификации не видны вызывающим методом. (this передается по значению)

Следовательно, идентичный код в итераторе и неитераторе дает разные результаты:

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];
    }
}

Вывод:

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

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

Почему это не ошибка компилятора (или, по крайней мере, предупреждение) для мутации структуры в итераторе?
Это поведение - тонкая ловушка, которая не легко понятна.

Анонимные методы, которые имеют одинаковое ограничение, не могут использовать this вообще.

Примечание: изменчивые структуры являются злыми; это никогда не должно возникать на практике.

4b9b3361

Ответ 1

Чтобы оправдать предупреждение, оно должно быть в ситуации, когда программист может получить неожиданные результаты. По словам Эрика Липперта, " мы стараемся резервировать предупреждения только в тех ситуациях, где мы можем с почти уверенностью сказать, что код сломан, вводит в заблуждение или бесполезен." Вот пример, в котором предупреждение будет вводить в заблуждение.

Скажем, у вас есть это совершенно верный - если не очень полезный - объект:

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;
    }
}

И у вас есть этот цикл:

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

Вы ожидаете, что этот цикл будет печатать 1,1,1,1, а не 1,2,3,4, правильно? Таким образом, класс работает точно так, как вы ожидаете. Это пример, когда предупреждение будет необоснованным.

Так как это явно не ситуация, когда код нарушается, вводит в заблуждение или бесполезен, как вы предлагаете компилятору генерировать ошибку или предупреждение?

Ответ 2

Чтобы ссылаться на себя, "изменчивые структуры злы":) То же самое, что и вы, произошло, если вы реализуете метод расширения для структуры. Если вы попытаетесь изменить структуру в методе расширения, у вас останется прежняя структура без изменений. Это немного менее удивительно, поскольку подпись метода расширения выглядит так:

static void DoSideEffects(this MyStruct x) { x.foo = ...

Глядя на это, мы понимаем, что происходит что-то вроде передачи параметров, и поэтому копируется структура. Но когда вы используете расширение, оно выглядит так:

x.DoSideEffects()

и вы будете удивлены тем, что не будете влиять на вашу переменную x. Я полагаю, что за кулисами ваши конструкции доходности делают что-то похожее на расширения. Я бы больше сформулировал стартовое предложение: "структуры злы".. в общем;)

Ответ 3

У меня была аналогичная мысль о том, что сказал Гейб. По-видимому, теоретически возможно, что можно использовать struct, чтобы вести себя подобно методу, инкапсулируя локальные переменные метода в качестве полей экземпляра:

struct Evens
{
    int _current;

    public IEnumerable<int> Go()
    {
        while (true)
        {
            yield return _current;
            _current += 2;
        }
    }
}

Я имею в виду, это странно, очевидно. Это напоминает мне идеи, с которыми я столкнулся в прошлом, однако, когда разработчики придумывали все более странные способы вызова методов, такие как перенос параметров метода в объект и позволяющий этому объекту вызвать метод, идущий назад, в чувство. Я бы сказал, что это примерно так.

Я не говорю, что это было бы разумным делом, но это, по крайней мере, способ использования this в том, как вы описываете, что может быть преднамеренным, и технически демонстрирует правильное поведение.

Ответ 4

Не совсем понятно, что должно произойти, хотя я думаю, что .net не нуждается в специальном атрибуте для методов, которые изменяют 'this'; такой атрибут может быть полезен применительно к неизменяемым типам классов, а также к изменяемым структурам. Методы, помеченные таким атрибутом, должны использоваться только для структурных переменных, полей и параметров, а не для временных значений.

Я не думаю, что есть способ избежать того, чтобы итератор захватил структуру по значению. Вполне возможно, что к моменту использования итератора исходная структура, на которой она была основана, может больше не существовать. С другой стороны, если структура реализовала интерфейс, который унаследовал IEnumerable <int> , но также включил функцию для возврата значения, приведение структуры к этому типу интерфейса перед использованием перечислителя в теории позволило бы итератору сохранить ссылку на структура без необходимости переопределять ее ценность; Я бы не удивился, если бы перечислитель копировал структуру по значению даже в этом случае.