Энумераторы
- Арифметическая последовательность
- Ограничиваем последовательность
- Пробрасываем количество элементов
- Выводим преобразованный counter
- Итого
В 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; // выдаем единичку
}
}
/* ... */
}
пробуем запустить:
а, ну точно! Ведь энумертор работает по следующему принципу:
- Вызываю MoveNext, если он вернул True значит элементы в последовательности еще есть
- Выдаю значение Current
- Снова вызываю MoveNext, если он вернул True значит элементы в последовательности еще есть
- Снова выдаю значение Current
- ….
- и так до тех пор, пока 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();
}
}