Foreach c как работает
Перейти к содержимому

Foreach c как работает

Операторы итераций (справочник по C#)

Следующие операторы многократно выполняют инструкцию или блок инструкций:

    : выполняет тело, а заданное логическое выражение принимает значение true . : перечисляет элементы коллекции и выполняет тело для каждого элемента коллекции. : условно выполняет тело один или несколько раз. : условно выполняет тело ноль или более раз.

Цикл можно прервать в любой момент в теле оператора итерации с помощью оператора break. Кроме того, можно перейти к следующей итерации в цикле с помощью оператора continue.

Инструкция for

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

В предыдущем примере показаны элементы оператора for :

Раздел инициализатора, который выполняется только один раз перед входом в цикл. Как правило, в этом разделе объявляется и инициализируется локальная переменная цикла. Доступ к объявленной переменной извне оператора for невозможен.

В разделе инициализатора в предыдущем примере объявляется и инициализируется целочисленная переменная-счетчик:

Раздел условия, в котором определяется, следует ли выполнять следующую итерацию в цикле. Если для него получено значение true или значение отсутствует, выполняется следующая итерация; в противном случае цикл завершается. Раздел условия должен быть логическим выражением.

В разделе условия в предыдущем примере проверяется, меньше ли трех значение счетчика.

Раздел итератора, который определяет, что происходит после каждого выполнения тела цикла.

Раздел итератора в предыдущем примере увеличивает значение счетчика:

Тело цикла которое должно быть оператором или блоком операторов.

Раздел итератора может содержать ноль или более следующих выражений оператора, разделенных запятыми:

  • префиксное или постфиксное выражение приращения, такое как ++i или i++
  • префиксное или постфиксное выражение декремента, такое как —i или i—
  • вызов метода
  • выражение await
  • создание объекта с помощью оператора new

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

Все разделы оператора for необязательны. Например, в следующем коде определяется бесконечный цикл for :

Инструкция foreach

Оператор foreach выполняет оператор или блок операторов для каждого элемента в экземпляре типа, который реализует интерфейс System.Collections.IEnumerable или System.Collections.Generic.IEnumerable<T>, как показано в следующем примере.

Оператор foreach не ограничен этими типами. Его можно использовать с экземпляром любого типа, который удовлетворяет следующим условиям:

  • Тип имеет открытый метод без параметров GetEnumerator . Начиная с C# 9.0 метод GetEnumerator может быть методом расширения типа.
  • тип возвращаемого значения метода GetEnumerator должен содержать открытое свойство Current и открытый метод MoveNext без параметров с типом возвращаемого значения bool .

В следующем примере показано использование оператора foreach с экземпляром типа System.Span<T>, который не реализует интерфейс:

Начиная с версии C# 7.3, если свойство перечислителя Current возвращает ссылочное возвращаемое значение ( ref T , где T — это тип элемента коллекции), вы можете объявить переменную итерации с модификатором ref или ref readonly , как показано в следующем примере.

Если оператор foreach применяется к null , возникает исключение NullReferenceException. Если исходная коллекция инструкции foreach пуста, тело оператора foreach не выполняется и пропускается.

await foreach

Начиная с C# 8.0, можно применять оператор await foreach для использования асинхронного потока данных, то есть типа коллекции, реализующего интерфейс IAsyncEnumerable<T>. Каждую итерацию цикла можно приостановить, пока будет осуществляться асинхронное извлечение следующего элемента. В следующем примере показано использование оператора await foreach .

Оператор await foreach можно также использовать с экземпляром любого типа, который удовлетворяет следующим условиям:

  • Тип имеет открытый метод без параметров GetAsyncEnumerator . Этот метод может быть методом расширения типа.
  • Тип возвращаемого значения метода GetAsyncEnumerator имеет открытое свойство Current и открытый метод без параметров MoveNextAsync , тип возвращаемого значения которого — Task<bool> , ValueTask<bool> или любой другой подтверждающий ожидание тип, метод ожидания которого GetResult возвращает значение bool .

Элементы потока по умолчанию обрабатываются в захваченном контексте. Чтобы отключить захват контекста, используйте метод расширения TaskAsyncEnumerableExtensions.ConfigureAwait. Дополнительные сведения о контекстах синхронизации и захвате текущего контекста см. в статье Использование асинхронного шаблона, основанного на задачах. Дополнительные сведения об асинхронных потоках см. в разделе Асинхронные потоки статьи Новые возможности в C# 8.0.

Тип переменной итерации

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

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

В предыдущей форме тип T элемента коллекции должен быть неявно или явно преобразован в тип V переменной итерации. Если явное преобразование из T в V завершается ошибкой во время выполнения, оператор foreach выдает исключение InvalidCastException. Например, если T является незапечатанным типом класса, V может быть любым типом интерфейса, даже тем, который T не реализует. Во время выполнения тип элемента коллекции может быть производным от T и фактически реализовать V . В противном случае возникает InvalidCastException.

Инструкция do

Оператор do выполняет оператор или блок операторов, пока определенное логическое выражение равно значению true . Так как это выражение оценивается после каждого выполнения цикла, цикл do выполняется один или несколько раз. Это отличает его от цикла while, который выполняется от нуля до нескольких раз.

В следующем примере показано применение оператора do .

Инструкция while

Оператор while выполняет оператор или блок операторов, пока определенное логическое выражение равно значению true . Так как это выражение оценивается перед каждым выполнением цикла, цикл while выполняется ноль или несколько раз. Это отличает его от цикла do, который выполняется от одного до нескольких раз.

В следующем примере показано применение оператора while .

Спецификация языка C#

Дополнительные сведения см. в следующих разделах статьи Спецификация языка C#:

Дополнительные сведения о функциях, добавленных в C# 8.0 и более поздние версии, см. в следующих заметках о функциях.

Интересные моменты в C# (foreach)

В этой статье мы коротко пройдемся по особенностям foreach. Первый момент вы скорее всего знаете, второй момент вы скорее всего не знаете.

Первый момент

На собеседованиях часто спрашивают — «Что необходимо сделать что бы ваш класс работал с foreach ?». Ответ на этот вопрос обычно звучит так — «Реализовать IEnumerable ». Ответ этот правильный, но не полный. В принципе, этого ответа на собеседовании достаточно и я ни разу не встречал чтобы кто то считал его неправильным. На самом деле, foreach использует «утиную типизацию». Для того чтобы наш класс работал в foreach достаточно иметь метод GetEnumerator возвращающий нечто имеющее метод MoveNext и свойство Current .

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

Примеры

Ошибка компилятора:
foreach statement cannot operate on variables of type ‘Container’ because ‘Container’ does not contain a public definition for ‘GetEnumerator’

Добавим метод GetEnumerator в контейнер и класс энумератора.

Ошибка компилятора:
foreach requires that the return type ‘Enumerator’ of ‘Container.GetEnumerator()’ must have a suitable public MoveNext method and public Current property

Добавим метод MoveNext и свойство Current в энумератор.

Теперь компилятор все устраивает.

Примечание:
Свойство Current может возвращать любой тип, как ref type так и value type . Собственно это и стало причиной использования «утиной типизации», во времена когда не было generics , для избежания ненужных boxing и unboxing .

Второй момент

На собеседовании встречаются вопросы про IDisposable и помимо общих вопросов про ручное управление ресурсами есть вопрос про то, в каких случаях компилятор может автоматически вызывать метод Dispose . Ответ все мы знаем — Dispose вызывается автоматически при использовании оператора using() . Ответ этот правильный, но неполный! Метод Dispose может вызывается в двух случаях, помимо using() , он вызывается в foreach для энумератора, если энумератор реализует IDisposable .

Примеры

Энумератор с Dispose :

Теперь при запуске примера мы увидим строчку «Dispose» в консоли.

Для тех кому интересно, вот код который генерит компилятор для нашего случая:

Объясните, как работают for и foreach, для чего они нужны?

Начал самообучение C#.
Никак не пойму, для чего нужны for, foreach (точнее, вроде как понимаю, но не совсем). Покажите наглядный пример для чего они нужны, понятный даже для того кто не учит яп.

user avatar

С нуля так с нуля. Значит пойдем с самого начала.

Для начала разберемся с циклом for

Возьмем самый простой пример, допустим вы хотите вывести числа от 0 до 9 построчно.

Очевидно, что можно сделать так:

Результат конечно достигнут, но что делать если чисел не 10, а 10000?

Ок, попробуем обойтись совсем старыми средствами (данный код строго демонстрационный, хоть он и работает, использовать его не надо нигде и никогда)

Цель достигнута? Безусловно. Но почему этот код плохой? Представьте что в вашей программе не одна метка, а пара десятков, запутаться какая для чего и когда именно на нее будет возврат проще простого. Читать такой код очень тяжело, искать ошибки еще тяжелее. Хотя надо отметить что на самом нижнем уровне оно именно так и работает, но у нас же язык высокого уровня, значит должны быть более простые и понятные человеку конструкции. И они есть, в первую очередь универсальный, во всех смыслах, цикл while :

От предыдущего кода отличается только отсутствием метки и ключевым словом while вместо if . Границы блока, который будет выполняться несколько раз, задают фигурные скобки. Хорошо? И да, и нет, потому что у нас все еще остается место потенциальной ошибки — это изменение счетчика. Оно должно происходить в самом конце повторяющегося блока, а условие выполнения находится в самом начале. Если кода в повторяющемся блоке много, то можно случайно забыть про необходимость поменять счетчик или изменить его не так как нужно и цикл превратится в вечный.

Последняя проблема решается с помощью цикла for :

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

Для компилятора и CLR, которая будет выполнять ваш код, разницы между этими вариантами абсолютно никакой, но исходный код программы должен быть понятен человеку, который будет его читать и отлаживать. Поэтому в рассмотренном примере использование цикла ‘for’ предпочтительнее.

Теперь посмотрим что скрывается за конструкцией foreach . Некоторые вещи будут вам незнакомы, поэтому для начала сравните понятность полученного кода, а потом разберитесь с незнакомыми классами и методами с помощью документации.

И так, допустим у нас есть некоторая коллекция объектов, например чисел List<int> . Нам не важно какие там числа, в каком порядке, мы просто хотим вывести их все. Разумеется можно воспользоваться циклом for и доставать элементы из коллекции по их порядковому номеру.

Однако, далеко не все коллекции позволяют обращаться к своим элементам по порядковому номеру, т.к. этого номера может и не быть. А еще есть перечисления ( IEnumerable , IEnumerable<T> ), у которых даже своих элементов нет, они предоставляют избранные элементы какой-то другой коллекции или вообще генерируют их налету. Что делать? Воспользуемся перечислителем ( IEnumerator , IEnumerator<T> ) и циклом while :

Цель достигнута? Да, но. для того, чтобы понять с чем мы вообще работаем, нужно найти в коде объявление нашей коллекции и посмотреть из каких элементов она состоит. В общем работает, но читать такое, мягко говоря, не комфортно. А если код не такой простой как в нашем примере, то вообще мрак.

Вот тут и понадобится цикл foreach :

Не трудно заметить, что в таком варианте однозначно понятен и тип элементов, с которыми будем работать, и из какой они коллекции, и ни какого вспомогательного кода в явном виде не присутствует. Разумеется, как и в случае с циклом for , компилятор все равно развернет ваш foreach именно в ту, неудобную для чтения, конструкцию, да еще и с метками, вместо понятного while , но это проблемы компилятора, своей цели мы добились — код краток и понятен человеку.

Ну вот как-то так. Если кто-то может дополнить и/или найдет неточности, дайте знать в комментариях, внесу правки.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *