Операторы итераций (справочник по 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 (точнее, вроде как понимаю, но не совсем). Покажите наглядный пример для чего они нужны, понятный даже для того кто не учит яп.
![]()
С нуля так с нуля. Значит пойдем с самого начала.
Для начала разберемся с циклом 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 этого должно быть достаточно.