Итератор использует две специальных инструкции:
yield return: определяет возвращаемый элемент
yield break: указывает, что последовательность больше не имеет элементов
Рассмотрим небольшой пример:
Numbers numbers = new Numbers(); foreach (int n in numbers) { Console.WriteLine(n); } class Numbers { public IEnumerator<int> GetEnumerator() { for (int i = 0; i < 6; i++) { yield return i * i; } } }
В классе Numbers метод GetEnumerator() фактически представляет итератор. С помощью оператора yield return возвращается некоторое значение (в данном случае квадрат числа).
В программе с помощью цикла foreach мы можем перебрать объект Numbers как обычную коллекцию. При получении каждого элемента в цикле foreach будет срабатывать оператор yield return, который будет возвращать один элемент и запоминать текущую позицию.
Благодаря итераторам мы можем пойти дальше и легко реализовать перебор числа в цикле foreach:
foreach(var n in 5) Console.WriteLine(n); foreach (var n in -5) Console.WriteLine(n); static class Int32Extension { public static IEnumerator<int> GetEnumerator(this int number) { int k = (number > 0)? number: 0; for (int i = number - k; i <= k; i++) yield return i; } }
В данном случае итератор реализован как метод расширения для типа int или System.Int32. В методе итератора фактически возвращаем все целочисленные значения от 0 до текущего числа. Консольный вывод:
0 1 2 3 4 5 -5 -4 -3 -2 -1 0
Другой пример: пусть у нас есть коллекция Company, которая представляет компанию и которая хранит в массиве personnel штат сотрудников - объектов Person. Используем оператор yield для перебора этой коллекции:
class Person { public string Name { get; } public Person(string name) =>Name = name; } class Company { Person[] personnel; public Company(Person[] personnel) => this.personnel = personnel; public int Length => personnel.Length; public IEnumerator<Person> GetEnumerator() { for (int i = 0; i < personnel.Length; i++) { yield return personnel[i]; } } }
Метод GetEnumerator()
представляет итератор. И когда мы будем осуществлять перебор в объекте Company в цикле foreach, то будет идти обращение к вызову yield return personnel[i];
. При
обращении к оператору yield return
будет сохраняться текущее местоположение. И когда метод foreach перейдет к следующей итерации
для получения нового объекта, итератор начнет выполнения с этого местоположения.
Ну и в основной программе в цикле foreach выполняется собственно перебор, благодаря реализации итератора:
var people = new Person[] { new Person("Tom"), new Person("Bob"), new Person("Sam") }; var microsoft = new Company(people); foreach(Person employee in microsoft) { Console.WriteLine(employee.Name); }
Хотя при реализации итератора в методе GetEnumerator()
применялся перебор массива в цикле for, но это необязательно делать.
Мы можем просто определить несколько вызовов оператора yield return
:
public IEnumerator<Person> GetEnumerator() { yield return personnel[0]; yield return personnel[1]; yield return personnel[2]; }
В этом случае при каждом вызове оператора yield return
итератор также будет запоминать текущее местоположение и при последующих вызовах начинать с него.
Выше для создания итератора мы использовали метод GetEnumerator
. Но оператор yield
можно использовать
внутри любого метода, только такой метод должен возвращать объект интерфейса IEnumerable
. Подобные методы еще называют
именованными итераторами.
Создадим такой именованный итератор в классе Company и используем его:
class Person { public string Name { get; } public Person(string name) =>Name = name; } class Company { Person[] personnel; public Company(Person[] personnel) => this.personnel = personnel; public int Length => personnel.Length; public IEnumerable<Person> GetPersonnel(int max) { for (int i = 0; i < max; i++) { if (i == personnel.Length) { yield break; } else { yield return personnel[i]; } } } }
Определенный здесь итератор - метод IEnumerable GetPersonnel(int max)
в качестве параметра принимает количество выводимых объектов.
В процессе работы программы может сложиться, что его значение будет больше, чем длина массива personnel. И чтобы не произошло ошибки, используется
оператор yield break. Этот оператор прерывает выполнение итератора.
Применение итератора:
var people = new Person[] { new Person("Tom"), new Person("Bob"), new Person("Sam") }; var microsoft = new Company(people); foreach(Person employee in microsoft.GetPersonnel(5)) { Console.WriteLine(employee.Name); }
Вызов microsoft.GetPersonnel(5)
будет возвращать набор из не более чем 5 объектов Person. Но так как у нас всего три таких объекта,
то в методе GetPersonnel
после трех операций сработает оператор yield break
.