Поиск по сайту:

Как работают перечислители и перечислители в 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, а затем продолжить обработку. Таким образом, сопрограмма полностью остановится, пока не сможет снова запуститься.