Энумераторы

В C# есть возможность создавать свои типы по к которым можно применять цикл foreach.

Зачем это нужно? Ну чтобы было по чему провести контрольную, очевидно =О

Еще пригождается, когда вы делаете обертку для какого-нибудь перечислимого типа.

Например, класс List, которые является динамическим списком, является оберткой простого массива.

И если вы, например, создадите свой класс обертку, допустим так

class MyList {
    int[] data = new int[] { 1, 2, 3, 4 };
}

и вам вдруг захочется обойти элементы этого списка, то написать вот так не получится

var list = new MyList();
foreach (var el in list)
{
    Console.WriteLine(el);
}

точнее написать то получится, но оно не скомпилируется, потому что компилятор не понимает, чего вы от него хотите.

Может вам хочется вывести все четные элементы, или только первый, или задом-наперед.

В общем надо как-то объяснить компилятору.

Для этого был придуман механизм энумераторов, который представляет собой набор интерфейсов, реализуя которые вы определяете последовательный порядок выдачи элементов в цикле foreach.

Первый интерфейс IEnumerable, выглядит вот так

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

второй интерфейс это IEnumerator, он отвечает за то каким образом последовательно выдавать элементы. Выглядит так

public interface IEnumerator
{
    object Current { get; } // должен выдавать текущий элемент последовательности 
    bool MoveNext(); // проверяет есть ли очередной элемент последовательности
    void Reset(); // сбрасывает цикл на начало
}

Он уже встроен во все перечислимые типы, на вроде массивов и списков.

И поэтому если хочется реализовать перебор внутреннего массива, все что надо сделать это наследовать IEnumerable и внутри GetEnumerator выдавать enumerator массива

вот так:

class MyList : IEnumerable // наследую интерфейс
{
    int[] data = new int[] { 1, 2, 3, 4 };
    

    // пробрасываю энумератор
    IEnumerator IEnumerable.GetEnumerator()
    {
        return data.GetEnumerator();
    }
}

и запуск цикла

var list = new MyList();
foreach (var el in list)
{
    Console.WriteLine(el);
}

выведет

> 1
> 2
> 3
> 4

я могу подтюнить поведение и, например, выдавать элементы в обратном порядке

class MyList : IEnumerable
{
    int[] data = new int[] { 1, 2, 3, 4 };

    IEnumerator IEnumerable.GetEnumerator()
    {
        return data.Reverse().GetEnumerator();
    }
}

выведет

> 4
> 3
> 2
> 1

могу только четные элементы выдать

class MyList : IEnumerable
{
    int[] data = new int[] { 1, 2, 3, 4 };

    IEnumerator IEnumerable.GetEnumerator()
    {
        return data.Where(x => x % 2 == 0).GetEnumerator();
    }
}

выдаст

> 2
> 4

могу в квадраты превратить

class MyList : IEnumerable
{
    int[] data = new int[] { 1, 2, 3, 4 };

    IEnumerator IEnumerable.GetEnumerator()
    {
        return data.Where(x => x % 2 == 0)
            .Select(x => x * 2)
            .GetEnumerator();
    }
}

получу

> 4
> 16

Но это все работает только если мы делаем своего рода прокси объект.

Когда же нам хочется сделать что-то более хитрое нам уже надо переопределять энумератор.

Арифметическая последовательность

Допустим я хочу создать класс, который будет выдавать элементы арифметической последовательности

class ArithmeticSequence {
}

запускаю я по нему цикл,

var seq = new ArithmeticSequence();

foreach (var el in seq)
{
    Console.WriteLine(el);
}

и он начинает выводить

> 1
> 2
> 3
> ...

потенциально до бесконечности.

Как мне это сделать?

Сначала я наследую IEnumerable, причем я буду использовать дженерик версию, в которой можно указать тип.

У меня последовательность целых чисел. Стало быть, и дженерик укажу как int

class ArithmeticSequence : IEnumerable<int> // наследовал дженерик интерфейс
{
    // добавляю метод который должен вернуть энумератор
    public IEnumerator<int> GetEnumerator() 
    {
        throw new NotImplementedException(); // пока просто ошибку выдаю
    }
    
    // в C#, в силу исторических причин надо добавлять еще и этот метод, 
    // но использоваться он не будет
    // так что просто игнорируйте его...
    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

так за логику генерации чисел отвечает IEnumerator, то значит нам надо создать класс энумератор для нашей последовательности.

Добавим его:

class ArithmeticSequenceEnumerator : IEnumerator<int>
{

}

и также добавим реализацию всех методов

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    // проверка, можно ли двигаться дальше
    public bool MoveNext()
    {
        throw new NotImplementedException();
    }
    
    // выдача очередного элемента
    public int Current
    {
        get
        {
            throw new NotImplementedException();
        }
    }
    
    // сброс 
    public void Reset()
    {
        throw new NotImplementedException();
    }
    
    // вот эти два оставшихся внизу не используются, так что игнорируем их
    object IEnumerator.Current => throw new NotImplementedException();
    public void Dispose()
    {
        // главное тут пустоту оставить
    }
}

подключим энумератор к последовательности, вот так:

class ArithmeticSequence : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator() 
    {
        return new ArithmeticSequenceEnumerator(); // выдаем экземпляр класса энумератора
    }
    
    /* ... */
}

теперь попробуем заставить наш энумератора что-то выдавать, ну пусть от единички выдает

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    public bool MoveNext()
    {
        throw new NotImplementedException();
    }
    
    public int Current
    {
        get
        {
            return 1; // выдаем единичку
        }
    }
    
    /* ... */ 
}

пробуем запустить:

а, ну точно! Ведь энумертор работает по следующему принципу:

  1. Вызываю MoveNext, если он вернул True значит элементы в последовательности еще есть
  2. Выдаю значение Current
  3. Снова вызываю MoveNext, если он вернул True значит элементы в последовательности еще есть
  4. Снова выдаю значение Current
  5. ….
  6. и так до тех пор, пока MoveNext не выдаст False

это значит, что если я буду выдавать в MoveNext все время true я получу потенциально бесконечную последовательность.

Попробуем:

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    public bool MoveNext()
    {
        return true;  // выдаю true
    }
    
    public int Current
    {
        get
        {
            return 1;
        }
    }
    
    /* ... */ 
}

запускаем:

Ура! =)

Теперь попробуем выдавать что-нибудь поумнее. Например, всегда возрастающее значение. Ну чтобы получилось

> 1
> 2
> 3
> ...

для этого заведем поле и будем его возвращать в Current

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    int counter = 0; // завел поле

    public bool MoveNext()
    {
        return true;
    }
    public int Current
    {
        get
        {
            return counter; // теперь вместо 1 возвращаю counter
        }
    }
    
    /* ... */

}

если запустить, то получу бесконечный поток нулей:

Я знаю, что энумератор циклично вызывает

  • MoveNext
  • Current
  • MoveNext
  • Current

Стало быть, я могу взять и, например, внутри MoveNext увеличивать значение counter на 1, вот так:

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    int counter = 0;

    public bool MoveNext()
    {
        counter += 1; // буду тут увеличивать на 1
        return true;
    }
 
    /* ... */

}

уиииии!

Ограничиваем последовательность

Генерить бесконечно много чисел, это конечно классно, но иногда неплохо бы их как-нибудь ограничить.

Как это сделать?

Попробуем сначала средствами энумератора. Добавим поле в котором укажем максимально количество элементов:

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    int counter = 0;
    int max = 10; // добавил

    /* ... */

}

естественно если это просто так запустить, то ничего не поменяется, получим туже последовательность

помните, я говорил, что MoveNext отвечает за остановку последовательности.

Так вот сделаем так, чтобы он не всегда возвращал true, а проверял условие

public bool MoveNext()
{
    counter += 1;
    if (counter <= max)
    {
        return true;
    } 
    else
    {
        return false;
    }
}

что конечно сильно многословно и можно записать так:

public bool MoveNext()
{
    counter += 1;
    return counter <= max ? true : false;
}

а это в свою очередь можно упростить до

public bool MoveNext()
{
    counter += 1;
    return counter <= max;
}

о как:

работает! =)

Пробрасываем количество элементов

Чтобы можно было указать количество элементов в последовательности надо чтобы мы могли менять значения max.

Для этого добавим конструктор, в котором можно будет указать max

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    int counter = 0;
    int max = 10;
    
    // добавил конструктор
    public ArithmeticSequenceEnumerator(int max)
    {
        this.max = max;
    }
    
    /* ... */

}

правда теперь ругается сама последовательность

чтобы она не ругалась надо передавать количество элементов в энумератор.

Для этого так же создадим поле max, но уже в последовательности, и добавим соответствующий конструктор, от так:

class ArithmeticSequence : IEnumerable<int>
{
    int max = 0; // добавил поле

    // добавил конструктор
    public ArithmeticSequence(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        return new ArithmeticSequenceEnumerator(max); // пробросил max в энумератор
    }

    /* ... */
}

теперь если я хочу получить 7 элементов последовательности, то надо написать так:

var seq = new ArithmeticSequence(7);
foreach (var el in seq)
{
    Console.WriteLine(el);
}

работает:

Выводим преобразованный counter

Кстати мы вот выводили просто последовательно элементы, но ведь это необязательно так делать. Мы можем, например, выводить квадраты элементов. Для этого надо просто запихать в Current формулу, вот так:

public int Current
{
    get
    {
        int x2 = counter * counter; // рассчитал квадрат
        return x2; // выдал его как текущий элемент
    }
}

вот так:

Попробуйте теперь ограничить последовательность не только сверху ну и снизу, например я хочу чтобы мне выдались с 3 по 5 элементы

То есть последовательность, принимает уже два параметра в конструктор

var seq = new ArithmeticSequence(3, 5);

будет выдавать

> 9
> 16
> 25

если возникнут какие вопросы, то смело задавайте вопросы в вк mkatash

Итого

Если вдруг где запутались, то получился такой код

class ArithmeticSequence : IEnumerable<int>
{
    int max = 0;

    public ArithmeticSequence(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        return new ArithmeticSequenceEnumerator(max);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

class ArithmeticSequenceEnumerator : IEnumerator<int>
{
    int counter = 0;
    int max = 10;

    public ArithmeticSequenceEnumerator(int max)
    {
        this.max = max;
    }

    public bool MoveNext()
    {
        counter += 1;
        return counter <= max ? true : false;
    }
    public int Current
    {
        get
        {
            int x2 = counter * counter;
            return x2;
        }
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }
    object IEnumerator.Current => throw new NotImplementedException();
    public void Dispose()
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        var seq = new ArithmeticSequence(7);
        foreach (var el in seq)
        {
            Console.WriteLine(el);
        }


        Console.ReadKey();
    }
}