0Pricing
C# Academy · Урок

Пользовательские перечисляемые типы

Реализуйте 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 — локальная установка не требуется.

Все уроки этого курса

  1. Методы-итераторы с yield return
  2. yield break и досрочное завершение
  3. Семантика ленивого вычисления
  4. Пользовательские перечисляемые типы
← Назад к C# Academy