Почему итераторы в структурах могут изменить это? - PullRequest
11 голосов
/ 23 декабря 2010

Я обнаружил, что методам итераторов в типах значений разрешено изменять 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 .

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

Ответы [ 4 ]

2 голосов
/ 24 декабря 2010

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

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

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

x.DoSideEffects()

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

2 голосов
/ 23 декабря 2010

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

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

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, верно?Так что класс работает именно так, как вы ожидаете.Это тот случай, когда предупреждение было бы необоснованным.

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

0 голосов
/ 21 декабря 2011

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

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

0 голосов
/ 01 января 2011

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

struct Evens
{
    int _current;

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

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

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

...