Как работают перечислители и перечислители в C#?
Интерфейсы C# IEnumerable и IEnumerator используются такими коллекциями, как массивы и списки, для стандартизации методов циклического обхода и выполнения действий, таких как фильтрация и выбор с помощью операторов LINQ. Мы обсудим, как они работают и как их использовать.
Что такое перечисляемое?
Термин «Enumerable» определяет объект, который предназначен для итерации, проходя каждый элемент один раз по порядку. В C# Enumerable
– это объект, подобный массиву, списку или любой другой коллекции, которая реализует интерфейс IEnumerable
. Перечисления стандартизируют зацикливание коллекций и позволяют использовать синтаксис запросов LINQ, а также другие полезные методы расширения, такие как List.Where()
или List.Select()
.
Все, что требуется для интерфейса IEnumerable
, — это метод GetEnumerator()
, который возвращает IEnumerator
. Так что же делают счетчики?
Для IEnumerator
требуются два метода: MoveNext()
и Current()
. MoveNext – это метод, используемый для обхода каждого элемента с применением любой пользовательской логики итератора в процессе, а Current
– метод, используемый для получения текущего элемента после того, как MoveNext
сделанный. В итоге вы получите интерфейс, определяющий объекты, которые можно перечислять, и способы обработки этого перечисления.
Например, каждый раз, когда вы пишете цикл foreach
, вы используете перечислители. На самом деле вы не можете использовать foreach
для коллекции, которая не реализует IEnumerable
. Когда он скомпилирован, цикл foreach
выглядит следующим образом:
foreach (Int element in list) { // your code }
Превращается в цикл while, который обрабатывается до тех пор, пока в Enumerable не закончатся элементы, каждый раз вызывая MoveNext
и устанавливая для переменной итератора значение .Current
.
IEnumerator enumerator= list.GetEnumerator(); while (list.MoveNext()) { element = (Int)enumerator.Current // your code }
В большинстве случаев вам нужно вручную реализовать все это, поскольку Enumerables, такие как списки, предназначены для использования в циклах и с методами LINQ. Но если вы хотите создать свой собственный класс коллекции, вы можете сделать это, реализовав IEnumerable и вернув перечислитель списка, в котором вы храните данные.
Для классов, в которых есть поля, которые вы хотели бы повторить, это может быть простым способом очистки кода. Вместо того, чтобы сделать массив данных общедоступным и перебирать CustomCollection.dataArray
, вы можете просто перебирать сам CustomCollection
. Конечно, если вы хотите, чтобы он вел себя как обычный список, вам также потребуется реализовать другие интерфейсы, такие как ICollection.
Запросы LINQ используют отложенное выполнение
Самое важное, что следует отметить в отношении LINQ, это то, что запросы не всегда выполняются сразу. Многие методы возвращают Enumerable и используют отложенное выполнение. Вместо того, чтобы зацикливаться сразу после выполнения запроса, он фактически ждет, пока Enumerable не будет использован в цикле, а затем выполняет запрос во время MoveNext
.
Давайте посмотрим на пример. Эта функция принимает список строк и печатает каждый элемент, длина которого превышает определенную длину. Он использует Where()
для фильтрации списка, а затем foreach
для вывода каждого элемента.
Этот синтаксис немного сбивает с толку, особенно если у вас есть привычка использовать var
(чего вам определенно не следует делать здесь), поскольку может показаться, что вы создаете новый список с Where()
и затем перебирать этот список. В случае цикла foreach IEnumerable может заменить список, но это не список — вы не можете добавлять или удалять элементы.
Однако C# умен и вместо того, чтобы перебирать список дважды (что удваивает время выполнения без всякой причины и выделяет бесполезную память), он просто отбрасывает логику фильтрации в цикл foreach
. Это не совсем то, во что транслируется ваш код, но было бы неплохо концептуализировать его таким образом, чтобы условие и фильтрация были разбиты внутри foreach.
Под капотом он делает это, добавляя запрос к Enumerator и возвращая Enumerable, который будет использовать логику из Where()
при вызове Enumerable.MoveNext()
. Если вам интересно, вы можете посмотреть, как System.Linq.Enumerable
обрабатывает это в System.Core
.
На самом деле это может иметь некоторые негативные последствия, если вы не будете осторожны. Что, если между Where()
и выполнением коллекция была изменена? Это может привести к тому, что запросы будут выполняться не по порядку.
Например, давайте дадим этой же функции некоторые входные данные, но вместо того, чтобы сразу печатать, мы добавим новое слово в список перед foreach
. На первый взгляд может показаться, что будут отфильтрованы отдельные буквы и выведено «Hello World», но на самом деле выводится «Hello World Banana», поскольку фильтрация не применяется до тех пор, пока не будет вызван MoveNext
во время фактической итерации foreach
.
Это особенно скрытно, поскольку мы даже не касаемся переменной toPrint
, которая передается в цикл foreach. Однако если вы вместо этого добавите .ToList()
в конец функции Where
, это заставит C# выполнить итерацию и вернуть новый список. Если это не то, что вам нужно, почти всегда лучше сэкономить время выполнения и выделение кучи, используя foreach
с Enumerable.
Что такое корутины?
Сопрограммы — это специальные функции, которые могут приостанавливать выполнение и возвращаться в основной поток. Они обычно используются для выполнения длительных действий, которые могут занять некоторое время, не вызывая зависания приложения во время ожидания подпрограммы. Например, в играх, где частота кадров приложения имеет большое значение, большие задержки, даже порядка нескольких миллисекунд, могут повредить пользовательскому опыту.
Сопрограммы довольно тесно связаны с перечислителями, поскольку на самом деле это просто функции, возвращающие перечислитель. Это определяет метод итератора, и вы можете его использовать. Вам не нужно много делать; просто измените возвращаемый тип функции на IEnumerator, и вместо того, чтобы вернуть
что-то, вы можете просто yield return
, который автоматически определяет ее как перечислитель, не делая этого явно.
Затем, чтобы разбить выполнение на разные кадры, вам просто нужно yield return null
всякий раз, когда вы хотите приостановить выполнение и запустить больше основного потока. Затем, чтобы использовать этот Enumerator, вам нужно будет вызвать функцию и назначить ее переменной, которую вы можете вызывать MoveNext()
через равные промежутки времени.
Если вы используете Unity, у них есть контроллер сопрограмм, который может выполнять обработку сопрограмм за вас, просто запуская и останавливая их своими методами. Однако на самом деле все, что он делает, — это просто вызывает MoveNext()
один раз за кадр, проверяет, может ли он обработать больше, и обрабатывает возвращаемое значение, если оно не равно нулю. Вы легко справитесь с этим сами:
Конечно, вам могут понадобиться дополнительные функции, такие как получение дочерних сопрограмм, которые вам нужно будет добавить в стек обработки, и создание выражений, таких как WaitForSeconds
или ожидание завершения асинхронных функций, которые вы просто нужно проверить один раз за кадр (перед вызовом MoveNext
), возвращает ли выражение значение true, а затем продолжить обработку. Таким образом, сопрограмма полностью остановится, пока не сможет снова запуститься.