Почему итераторы в структурах меняют это?
Я обнаружил, что методам итератора в типах значений разрешено изменять 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
вообще.
Примечание: изменчивые структуры являются злыми; это никогда не должно возникать на практике.
Ответы
Ответ 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> , но также включил функцию для возврата значения, приведение структуры к этому типу интерфейса перед использованием перечислителя в теории позволило бы итератору сохранить ссылку на структура без необходимости переопределять ее ценность; Я бы не удивился, если бы перечислитель копировал структуру по значению даже в этом случае.