В последнее время я много читал о foreach
и шаблоне итератора.
В ходе исследования у меня возникли следующие вопросы:
- Как переменная итерации foreach доступна только для чтения?
- Почему компилятор позволяет присваивать переменную итерации при непосредственном использовании шаблона итератора?
- Почему
IEnumerator.Current
только для чтения? - Почему я не могу добавлять/удалять элементы в
IEnumerable<T>
? - Почему среда выполнения позволяет обновлять свойство элемента в
IEnumerable<T>
? - Зачем нужен
IEnumerable<T>
, когда естьIEnumerator<T>
? - Когда используется
Array.GetEnumerator()
?
На этой странице будут рассмотрены эти вопросы и ответы, которые я получил от StackOverflow.
Как переменная итерации foreach доступна только для чтения?
Почему?
Ответить «почему» легко, потому что это избавляет вас от мыслей о том, что вы будете обновлять элемент в коллекции, чего вы бы не сделали, потому что вы имеете дело с локальной переменной, которая просто имеет ссылку на элемент в коллекции. коллекция.
Обновление ссылки в локальной переменной ничего не делает с объектом в коллекции.
Как?
«Как» было немного сложнее выяснить.
В 8.8.4 спецификации C# приведен следующий пример:
Инструкция foreach в форме:
foreach (V v in x) embedded-statement
затем расширяется до:
{ E e = ((C)(x)).GetEnumerator(); try { V v; while (e.MoveNext()) { v = (V)(T)e.Current; embedded-statement } } finally { … // Dispose e } }
Переменная итерации соответствует локальной переменной только для чтения с областью действия, которая распространяется на встроенный оператор. Переменная v во встроенном операторе доступна только для чтения.
Я был озадачен тем, как это возможно, когда в «расширенном» коде нет ключевых слов только для чтения.
Расширенный код — это C#-эквивалент созданного CIL-кода.
Нет необходимости иметь какой-либо явный способ пометить его как доступный только для чтения, потому что код CIL генерируется только в том случае, если код компилируется.
Код не будет компилироваться в первую очередь, если вы попытаетесь обновить переменную итерации.
В компиляторе есть правило, запрещающее вам это делать.
Почему компилятор позволяет присваивать переменную итерации при непосредственном использовании шаблона итератора?
Рассмотрим этот код:
Было бы неплохо, если бы компилятор мог предотвратить это. Однако логика, используемая компилятором для распознавания этой ситуации и предотвращения ее, была бы слишком сложной.
Компилятору легко узнать, когда есть попытка присвоить переменную итерации в foreach
, однако присваивание переменной в цикле while — это нормально, и было бы слишком сложно работать, если бы это был цикл while. это часть шаблона итератора.
Почему IEnumerator.Current доступен только для чтения?
Обратите внимание, что это другой вопрос, чем вопрос о том, почему переменная итерации foreach
доступна только для чтения.
Переменная итерации foreach
представляет свойство Current
и доступна только для чтения, потому что, если бы вы могли ее обновить, вы могли бы подумать, что обновляете элемент в коллекции, чего вы бы не сделали.
Однако что произойдет, если у свойства Current
будет установщик?
Опять же, это было бы бесполезно, так как тогда вам было бы позволено обновить свойство Current
, которое не коснется коллекции и которое позже все равно будет перезаписано при следующем вызове MoveNext()
.
Однако что, если в сеттере есть логика для обновления коллекции?
Это будет работать на обычных коллекциях, однако, поскольку вы не можете обновлять ссылки, хранящиеся в элементах в IEnumerable<T>
в foreach
, имеет смысл быть последовательным и предотвратить это также в шаблоне итератора.
Другая причина, по которой свойство Current
доступно только для чтения, заключается в том, что нет смысла обновлять элементы в некоторых коллекциях:
Если бы у вас был следующий код:
Что именно вы бы обновили? Нет никакого смысла обновлять этот тип коллекции.
Почему я не могу добавлять/удалять элементы в IEnumerable‹T›?
Это испортило бы итерацию коллекций.
Если бы это стало возможным, что бы вы ожидали, когда добавляете элемент в начало коллекции?
Должна ли следующая итерация цикла foreach
вернуться к началу, поскольку вы добавили новый элемент?
Из-за возможных странных ситуаций IEnumerable<T>
доступен только для чтения.
Почему среда выполнения позволяет обновлять свойство элемента в IEnumerable‹T›?
Рассмотрим этот код:
Как и в предыдущих примерах, где вы не можете обновить ссылку, хранящуюся в переменной итерации foreach
, теперь вы можете задать вопрос, почему среда выполнения не генерирует исключение, когда вы пытаетесь обновить свойство элемента, являющегося частью IEnumerable<T>
?
Обновление свойства элемента IEnumerable<T>
в foreach
бесполезно, поскольку обновляется только текущая проекция. т.е. в приведенном выше foreach вы обновляете объект, который сразу после итерации будет собирать мусор. Как только вы снова пройдете через IEnumerable<T>
, вы получите новую проекцию.
Это не может быть обнаружено компилятором, потому что было бы слишком сложно понять, является ли коллекция чистой IEnumerable<T>
или просто коллекцией, реализующей IEnumerable<T>
. Однако этому можно помешать, если среда выполнения выдаст Exception
при обнаружении попытки обновления объекта.
Причина, по которой Exception
не выбрасывается, заключается в том, что среда выполнения должна отслеживать тот факт, что этот объект был получен из перечисляемого. Он должен был бы отслеживать тип перечислимого (поскольку он должен был бы разрешать его, если бы он был основан на списке и т. д.), и он должен был бы отслеживать, была ли переменная назначена где-то еще (поскольку, если бы это было так, код потенциально мог бы быть легитимный).
Это просто слишком много накладных расходов для мало пользы.
Зачем нужен IEnumerable‹T›, когда есть IEnumerator‹T›?
IEnumerable<T>
— самый простой интерфейс. Он объявляет один метод, GetEnumerator()
.
Почему бы просто не использовать логику в IEnumerator<T>
вместо IEnumerable<T>
?
Думайте о IEnumerable<T>
как о фабрике по созданию IEnumerator<T>
.
Если бы вы сохранили состояние текущего местоположения перечислителя непосредственно в IEnumerable<T>
, вы не смогли бы использовать вложенное перечисление для коллекции.
К тому времени, когда вы доберетесь до своего вложенного цикла, свойство Current
будет считывать второй элемент, а не первый.
Когда используется Array.GetEnumerator()?
Некоторые разработчики думают, что всякий раз, когда вы используете foreach
, он генерирует код, который вызывает методы IEnumerator<T>
.
Это не всегда так. Если вы используете foreach
в массиве, он будет использовать цикл for за кулисами.
Рассмотрим этот код С#:
В эквивалентном коде CIL нет вызовов MoveNext()
и т.д., это просто цикл:
Поскольку массив не поддерживает добавление/удаление элементов, подразумевается фиксированный Length
.
Таким образом, без проверки границ это оптимизация для доступа к элементам массива по индексу (цикл for), а не по Итератору (реализация IEnumerable<T>
).
Тем не менее, есть еще причина иметь Array.GetEnumerator()
. Это предусмотрено для функциональности Linq:
В эквивалентном CIL-коде мы видим вызовы MoveNext()
и т. д.: