Итераторы и оператор yield

Последнее обновление: 19.01.2022
Итератор по сути представляет блок кода, который использует оператор yield для перебора набора значений. Данный блок кода может представлять тело метода, оператора или блок get в свойствах.

Итератор использует две специальных инструкции:

  • 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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850