Пользовательские перечисляемые типы
Реализуйте IEnumerable с помощью итераторов.
«Пользовательские перечисляемые типы» — бесплатный урок C# Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Создание собственного перечисляемого объекта
Реализовав IEnumerable<T> в классе, Вы сделаете его пригодным для использования в foreach. В сочетании с методами-итераторами это позволяет создавать пользовательские коллекции и диапазоны с очень небольшим объёмом кода.
Реализация GetEnumerator с помощью yield
Самый простой способ реализовать IEnumerable<T> — написать GetEnumerator как итератор с использованием yield return. Компилятор создаст перечислитель за Вас.
using System;
using System.Collections;
using System.Collections.Generic;
public class IntRange : IEnumerable<int>
{
private readonly int _start, _count;
public IntRange(int start, int count) { _start = start; _count = count; }
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < _count; i++)
yield return _start + i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
foreach (var n in new IntRange(5, 4))
Console.WriteLine(n);
}
}Зачем нужны два метода GetEnumerator
IEnumerable<T> наследует необобщённый IEnumerable, поэтому необходимо реализовать оба интерфейса. Явная необобщённая реализация просто делегирует вызов обобщённой версии.
using System;
using System.Collections;
using System.Collections.Generic;
public class Letters : IEnumerable<char>
{
public IEnumerator<char> GetEnumerator()
{
yield return 'x';
yield return 'y';
yield return 'z';
}
// Required because IEnumerable<T> extends IEnumerable
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
foreach (var c in new Letters())
Console.WriteLine(c);
}
}Инкапсуляция пользовательской логики
Пользовательский перечисляемый объект может скрыть сложные правила генерации за понятным типом. Здесь класс выдаёт последовательность Fibonacci заданной длины.
using System;
using System.Collections;
using System.Collections.Generic;
public class Fibonacci : IEnumerable<int>
{
private readonly int _count;
public Fibonacci(int count) { _count = count; }
public IEnumerator<int> GetEnumerator()
{
int a = 0, b = 1;
for (int i = 0; i < _count; i++)
{
yield return a;
int next = a + b; a = b; b = next;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
foreach (var n in new Fibonacci(8))
Console.WriteLine(n);
}
}Обёртка над внутренней коллекцией
Пользовательский перечисляемый объект часто оборачивает внутреннее хранилище, предоставляя перебор и одновременно контролируя добавление или фильтрацию элементов.
using System;
using System.Collections;
using System.Collections.Generic;
public class EvenBag : IEnumerable<int>
{
private readonly List<int> _items = new List<int>();
public void Add(int n) { if (n % 2 == 0) _items.Add(n); }
public IEnumerator<int> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
var bag = new EvenBag();
bag.Add(1); bag.Add(2); bag.Add(3); bag.Add(4);
foreach (var n in bag) Console.WriteLine(n);
}
}Пользовательские перечисляемые объекты работают с LINQ
Любой тип, реализующий IEnumerable<T>, сразу получает все возможности LINQ. Вашу пользовательскую коллекцию можно фильтровать, проецировать и агрегировать без дополнительного кода.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class IntRange : IEnumerable<int>
{
private readonly int _start, _count;
public IntRange(int start, int count) { _start = start; _count = count; }
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < _count; i++) yield return _start + i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
var range = new IntRange(1, 10);
Console.WriteLine(range.Where(n => n % 3 == 0).Sum());
}
}Каждый цикл foreach получает новый перечислитель
Поскольку GetEnumerator вызывается при каждом переборе, пользовательский перечисляемый объект можно перечислять многократно, начиная каждый проход заново.
using System;
using System.Collections;
using System.Collections.Generic;
public class IntRange : IEnumerable<int>
{
private readonly int _count;
public IntRange(int count) { _count = count; }
public IEnumerator<int> GetEnumerator()
{
for (int i = 1; i <= _count; i++) yield return i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
var r = new IntRange(3);
foreach (var n in r) Console.Write(n);
Console.WriteLine();
foreach (var n in r) Console.Write(n); // fresh again
Console.WriteLine();
}
}Обобщённый пользовательский перечисляемый объект
Сделайте перечисляемый объект обобщённым, чтобы он мог хранить элементы любого типа. Здесь простой объект ring предоставляет свои элементы по порядку, начиная с выбранного смещения.
using System;
using System.Collections;
using System.Collections.Generic;
public class Ring<T> : IEnumerable<T>
{
private readonly T[] _items;
private readonly int _start;
public Ring(T[] items, int start) { _items = items; _start = start; }
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < _items.Length; i++)
yield return _items[(_start + i) % _items.Length];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
var ring = new Ring<string>(new[] { "a", "b", "c", "d" }, 2);
foreach (var s in ring) Console.WriteLine(s);
}
}Ленивые пользовательские перечисляемые объекты
Поскольку GetEnumerator сам является итератором, Ваш пользовательский перечисляемый объект по умолчанию работает лениво: элементы создаются только по мере их запроса потребителем.
using System;
using System.Collections;
using System.Collections.Generic;
public class Squares : IEnumerable<int>
{
private readonly int _count;
public Squares(int count) { _count = count; }
public IEnumerator<int> GetEnumerator()
{
for (int i = 1; i <= _count; i++)
{
Console.WriteLine("computing " + i);
yield return i * i;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
foreach (var n in new Squares(3))
Console.WriteLine("got " + n);
}
}Когда стоит создавать пользовательский перечисляемый объект
Используйте пользовательский перечисляемый объект, когда порядок перебора или логика генерации являются важной частью типа, например при обходе дерева, работе с постраничным источником данных или математической последовательностью. В остальных случаях достаточно обычного метода-итератора.
using System;
using System.Collections;
using System.Collections.Generic;
public class Countdown : IEnumerable<int>
{
private readonly int _from;
public Countdown(int from) { _from = from; }
public IEnumerator<int> GetEnumerator()
{
for (int i = _from; i >= 0; i--) yield return i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
foreach (var n in new Countdown(5)) Console.WriteLine(n);
}
}Попробуйте сами
Создайте пользовательский перечисляемый объект, который выдаёт геометрическую последовательность, а затем примените к нему LINQ. Ваш тип сразу подключится ко всей экосистеме.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class Geometric : IEnumerable<int>
{
private readonly int _start, _ratio, _count;
public Geometric(int start, int ratio, int count) { _start = start; _ratio = ratio; _count = count; }
public IEnumerator<int> GetEnumerator()
{
int value = _start;
for (int i = 0; i < _count; i++) { yield return value; value *= _ratio; }
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class Program
{
public static void Main()
{
var seq = new Geometric(1, 2, 6);
Console.WriteLine(string.Join(", ", seq));
Console.WriteLine("sum: " + seq.Sum());
}
}Быстрая проверка
Вспомните контракт IEnumerable<T>.
Итоги
Пользовательский перечисляемый объект реализует IEnumerable<T>, поэтому работает с foreach и LINQ.
- Реализуйте
GetEnumeratorкак итератор с помощьюyield return. - Также предоставьте явную необобщённую реализацию
GetEnumerator. - Каждая итерация получает новый перечислитель.
- По умолчанию перебор выполняется лениво.
Часто задаваемые вопросы
Урок «Пользовательские перечисляемые типы» бесплатный?
Да — полный текст урока «Пользовательские перечисляемые типы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Пользовательские перечисляемые типы»?
Реализуйте IEnumerable с помощью итераторов. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Пользовательские перечисляемые типы»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Методы-итераторы с yield return
- yield break и досрочное завершение
- Семантика ленивого вычисления
- Пользовательские перечисляемые типы