Почему мы не можем назначить переменную итерации foreach, тогда как мы можем полностью изменить ее с помощью метода доступа?

Мне просто было любопытно: следующий код не скомпилируется, потому что мы не можем изменить переменную итерации foreach:

        foreach (var item in MyObjectList)
        {
            item = Value;
        }

Но следующее будет скомпилировано и запущено:

        foreach (var item in MyObjectList)
        {
            item.Value = Value;
        }

Почему первое недопустимо, тогда как второе может сделать то же самое внизу (я искал правильное английское выражение для этого, но я его не помню. Под...? ^^ )


person GianT971    schedule 20.10.2011    source источник
comment
Я думаю, вы имеете в виду под капотом, но на самом деле это совсем не то же самое.   -  person Jon Skeet    schedule 20.10.2011
comment
@ GianT971 Не могли бы вы описать, как бы вы «сделали то же самое» в своем сеттере? this = value?   -  person jv42    schedule 20.10.2011
comment
@JonSkeet Да под капотом ^^. Итак, элемент на самом деле является ссылкой на объект, а не на сам объект, теперь я понял   -  person GianT971    schedule 20.10.2011
comment
@ jv42 вы совершенно правы, я забыл, что два объекта с одинаковыми значениями всех свойств не означают, что они равны   -  person GianT971    schedule 20.10.2011


Ответы (9)


foreach - это итератор только для чтения, который динамически перебирает классы, реализующие IEnumerable, каждый цикл в foreach будет вызывать IEnumerable для получения следующего элемента, элемент, который у вас есть, является ссылкой только для чтения, вы не можете повторно назначать его, а просто вызываете item.Value обращается к нему и присваивает некоторое значение атрибуту чтения/записи, но все же ссылка на элемент является ссылкой только для чтения.

person Mohamed Abed    schedule 20.10.2011
comment
foreach — это итератор только для чтения, который динамически перебирает классы, реализующие IEnumerable Хотя это правильно, это не совсем верно. Компилятору требуется только, чтобы тип, который вы перебираете, имел методы T GetEnumerator(), bool MoveNext() и свойство T Current (при условии, что вы хотите создать свой собственный перечислитель). На самом деле вам не нужно реализовывать IEnumerable. - person Joseph Woodward; 25.11.2016
comment
@JosephWoodward: Хоть это и правильно, это не совсем так. Джозеф, это заявление уровня Йоды, которое определенно войдет в мою десятку лучших за все время! :D - person Jpsy; 07.12.2018
comment
В чем причина создания переменной итерации только для чтения, когда вы можете изменить объект, на который она ссылается? Вы также можете изменить ссылку на переменную, если хотите, удалив синтаксический сахар и используя цикл while и вызывая MoveNext() и т. д. - person David Klempfner; 18.09.2019
comment
потому что вы можете управлять уровнем доступа к свойствам индивидуально, например, делая что-то только с геттером, но без сеттера. Он обеспечивает детальный доступ к свойствам внутри объекта. - person Bon; 23.07.2020

Второй делает совсем не то. Это не изменение значения переменной item — это изменение свойства объекта, к которому относится это значение. Эти два значения будут только эквивалентны, если item является изменяемым типом значения, и в этом случае вы все равно должны его изменить, поскольку изменяемые типы значений являются злом. (Они ведут себя самым разным образом, чего неосторожный разработчик может и не ожидать.)

Это то же самое, что:

private readonly StringBuilder builder = new StringBuilder();

// Later...
builder = null; // Not allowed - you can't change the *variable*

// Allowed - changes the contents of the *object* to which the value
// of builder refers.
builder.Append("Foo");

Дополнительную информацию см. в моей статье о ссылках и значениях.

person Jon Skeet    schedule 20.10.2011

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

Используйте цикл for, если вам нужно добавить/удалить/изменить элементы в коллекции:

for (int i = 0; i < MyObjectList.Count; i++)
{
    MyObjectList[i] = new MyObject();
}
person James Johnson    schedule 20.10.2011
comment
Но если бы код скомпилировался, он все равно не изменил бы коллекцию, поскольку переменная итерации является локальной переменной, и вы просто обновили бы ссылку, на которую указывает эта локальная переменная. - person David Klempfner; 14.09.2019

Если вы посмотрите на спецификацию языка, вы поймете, почему это не работает:

В спецификациях говорится, что foreach расширяется до следующего кода:

 E e = ((C)(x)).GetEnumerator();
   try {
      V v;
      while (e.MoveNext()) {
         v = (V)(T)e.Current;
                  embedded-statement
      }
   }
   finally {
      … // Dispose e
   }

Как видите, текущий элемент используется для вызова MoveNext(). Поэтому, если вы измените текущий элемент, код «потеряется» и не сможет перебирать коллекцию. Таким образом, изменение элемента на что-то другое не имеет никакого смысла, если вы видите, какой код на самом деле создает компилятор.

person Wouter de Kort♦    schedule 20.10.2011
comment
Насколько я понимаю, vбудет содержать текущую ссылку на элемент, а e содержит ссылку на итератор. Таким образом, компилятор может просто назначить все, что мы хотим, o v, а e по-прежнему будет удерживать итератор. Таким образом, переменным элемента foreach могут быть назначены новые ссылки, поскольку они преобразуются в переменную v, а не e. - person Michel Feinstein; 16.09.2016

Можно сделать item изменяемым. Мы могли бы изменить способ создания кода так, чтобы:

foreach (var item in MyObjectList)
{
  item = Value;
}

Стало эквивалентно:

using(var enumerator = MyObjectList.GetEnumerator())
{
  while(enumerator.MoveNext())
  {
    var item = enumerator.Current;
    item = Value;
  }
}

И тогда он будет компилироваться. Однако на коллекцию это не повлияет.

И есть руб. Код:

foreach (var item in MyObjectList)
{
  item = Value;
}

Имеет два разумных способа для человека думать об этом. Во-первых, item — это просто заполнитель, и его изменение ничем не отличается от изменения item в:

for(int item = 0; item < 100; item++)
    item *= 2; //perfectly valid

Во-вторых, изменение элемента фактически изменит коллекцию.

В первом случае мы можем просто присвоить элемент другой переменной, а затем поиграть с ней, чтобы не было потерь. В последнем случае это одновременно запрещено (или, по крайней мере, вы не можете ожидать изменения коллекции во время итерации по ней, хотя это не должно применяться принудительно) всеми счетчиками) и во многих случаях невозможно обеспечить (в зависимости от характера перечислимого).

Даже если мы считаем первый случай «правильной» реализацией, тот факт, что он может быть разумно интерпретирован людьми двумя разными способами, является достаточно веской причиной, чтобы не допускать его, особенно учитывая, что мы можем легко обойти это в любом случае. .

person Jon Hanna    schedule 20.10.2011
comment
Хотя это кажется настоящим ответом, я вообще не нахожу последнюю интерпретацию разумной. - person Zeus; 24.06.2020

Потому что в первом нет особого смысла. Переменный элемент управляется итератором (устанавливается на каждой итерации). Вам не нужно его менять - просто используйте другую переменную:

foreach (var item in MyObjectList)
{
    var someOtherItem = Value;

    ....
}

Что касается второго, здесь есть допустимые варианты использования - вы можете перебрать перечисление автомобилей и вызвать .Drive() для каждого или установить car.gasTank = full;

person Chris Shain    schedule 20.10.2011

Дело в том, что вы не можете изменить саму коллекцию во время итерации по ней. Абсолютно законно и распространено изменение объектов, которые дает итератор.

person Florian Greinacher    schedule 20.10.2011

Потому что эти два не одно и то же. При выполнении первого вы можете ожидать изменения значения в коллекции. Но то, как работает foreach, сделать это невозможно. Он может только извлекать элементы из коллекции, но не устанавливать их.

Но когда у вас есть предмет, он становится таким же объектом, как и любой другой, и вы можете изменять его любым (разрешенным) способом.

person svick    schedule 20.10.2011

Используйте цикл for вместо цикла foreach и присвойте значение. это будет работать

foreach (var a in FixValue)
{
    for (int i = 0; i < str.Count(); i++)
    {
       if (a.Value.Contains(str[i]))
           str[i] = a.Key;
    }
 }
person SPnL    schedule 18.03.2017