Реализация интерфейсов IEnumerable и IEnumerator

Для циклического обращения к элементам коллекции зачастую проще (да и лучше) организовать цикл foreach, чем пользоваться непосредственно методами интерфейса IEnumerator. Тем не менее ясное представление о принципе действия подобных интерфейсов важно иметь по еще одной причине: если требуется создать класс, содержащий объекты, перечисляемые в цикле foreach, то в этом классе следует реализовать интерфейсы IEnumerator и IEnumerable. Иными словами, для того чтобы обратиться к объекту определяемого пользователем класса в цикле foreach, необходимо реализовать интерфейсы IEnumerator и IEnumerable в их обобщенной или необобщенной форме. Правда, сделать это будет нетрудно, поскольку оба интерфейса не очень велики.
Ниже приведен пример, в котором класс MyInt реализует данные интерфейсы:
using System; using System.Collections; namespace ConsoleApplication1 < class MyInt : IEnumerable, IEnumerator < int[] ints = < 12, 13, 1, 4 >; int index = -1; // Реализуем интерфейс IEnumerable public IEnumerator GetEnumerator() < return this; >// Реализуем интерфейс IEnumerator public bool MoveNext() < if (index == ints.Length - 1) < Reset(); return false; >index++; return true; > public void Reset() < index = -1; >public object Current < get < return ints[index]; >> > class Program < static void Main() < MyInt mi = new MyInt(); foreach (int i in mi) Console.Write(i+"\t"); Console.ReadLine(); >> >
IEnumerable интерфейс в C# и LSP
Эта статья — продолжение статьи C#: коллекции только для чтения и LSP. Сегодня мы посмотрим на интерфейс IEnumerable с точки зрения принципа подстановки Барбары Лисков (LSP), а также разберемся, нарушает ли этот принцип код, имплементирующий IEnumerable.
LSP и IEnumerable интерфейс
Чтобы ответить на вопрос, нарушают ли классы-наследники IEnumerable LSP принцип, давайте посмотрим, что как вообще можно нарушить этот принцип.
- Подкласс класса (или, в нашем случае, интерфейса) не сохраняет инварианты родителя
- Подкласс ослабляет постусловия родителя
- Подкласс усиливает предусловия родителя
Имплементации IEnumerable интерфейса
Прежде чем мы погрузимся в имплементации, давайте взглянем на сам интерфейс. Вот код интерфейсов IEnumerable, IEnumerator и IEnumerator. Интерфейс IEnumerable практически не отличается от IEnumerable.
public interface IEnumerable : IEnumerable < IEnumeratorGetEnumerator(); > public interface IEnumerator : IDisposable, IEnumerator < T Current < get; >> public interface IEnumerator < object Current < get; >bool MoveNext(); void Reset(); >
Они довольно просты. Тем не менее, различные классы BCL имплементируют их по-разному. Возможно, наиболее показательным примеромм будет имплементация в классе List.
public class List < public struct Enumerator : IEnumerator < private Listlist; private int index; private T current; public T Current < get < return this.current; >> object IEnumerator.Current < get < if (this.index == 0 || this.index == this.list._size + 1) throw new InvalidOperationException(); return (object)this.Current; >> > >
Свойство Current с типом T не требует вызова MoveNext(), в то время как свойство Current с типом object требует:
public void Test() < List.Enumerator enumerator = new List().GetEnumerator(); int current = enumerator.Current; // Возврашает 0 object current2 = ((IEnumerator)enumerator).Current; // Бросает exception >
Метод Reset() также реализован по-разному. В то время как List.Enumerator.Reset() добросовестно переводит Enumerator в начало списка, итераторы не имплементируют их вовсе, так что следующий код работать не будет:
public void Test() < Test2().Reset(); // Бросает NotSupportedException >private IEnumerator Test2()
Получается, что единственное, в чем мы можем быть уверены при работе с IEnumerable, это то, что метод IEnumerable.GetEnumerator() возвращает ненулевой (non-null) объект энумератора. Класс, имплементирующий IEnumerable, может быть как пустым множеством:
private IEnumerable Test2()
Так и бесконечной последовательностью элементов:
private IEnumerable Test2() < Random random = new Random(); while (true) < yield return random.Next(); >>
И это не выдуманный пример. Класс BlockingCollection имплементирует IEnumerator таким образом, что вызывающий поток блокируется на методе MoveNext() до тех пор, пока какой-нибудь другой поток не добавит элемент в коллекцию:
public void Test() < BlockingCollectioncollection = new BlockingCollection(); IEnumerator enumerator = collection.GetConsumingEnumerable().GetEnumerator(); bool moveNext = enumerator.MoveNext(); // The calling thread is blocked >
Другими словами, интерфейс IEnumerable не дает никаких гарантий о нижележащем множестве элементов, он даже не гарантирует, что это множество конечно. Все, что он нам говорит, — это то, что это множество может быть каким-то образом проитерировано.
IEnumerable и LSP
Итак, нарушают ли LSP классы, имплементирующие IEnumerable? Рассмотрим следующий пример:
public void Process(IEnumerable orders) < foreach (Order order in orders) < // Do something >>
В случае если нижележащий тип у переменной orders — List, все в порядке: элементы списка могут быть легко проитерированы. Но что если orders на самом деле представляет из себя бесконечный генератор, создающий новый объект каждый раз при вызове MoveNext()?
internal class OrderCollection : IEnumerable < public IEnumeratorGetEnumerator() < while (true) < yield return new Order(); >> >
Очевидно, метод Process не сработает как задумано. Но будет ли это из-за того, что класс OrderCollection нарушает LSP? Нет. OrderCollection скрупулезно следует контракту интерфейса IEnumerable: он предоставляет новый объект каждый раз, когда его просят об этом.
Проблема в том, что метод Process ожидает от объекта, реализующего IEnumerable, большего, чем этот интерфейс обещает. Нет никакой гарантии, что нижележащий класс переменной orders — конечная коллекция. Как я упомянул ранее, orders может быть экземпляром класса BlockingCollection, что делает бесполезными попытки проитерировать все его элементы.
Чтобы избежать проблем, мы можем просто изменить тип входящего параметра на ICollection. В отличие от IEnumerable, ICollection предоставляет свойство Count, которое гарантирует, что нижележащая коллекция конечна.
IEnumerable и коллекции только для чтения
Использование ICollection имеет свои недостатки. ICollection позволяет изменять свои элементы, что часто нежелательно если вы хотите использовать коллекцию как коллекцию только для чтения. До версии .Net 4.5, IEnumerable интерфейс часто использовался для этой цели.
В то время как это кажется хорошим решением, это накладывает слишком большие ограничения на подребителей интерфейса.
public int GetTheTenthElement(IEnumerable collection)
Это один из наиболее часто встречаемых подходов: использование LINQ для обхода ограничений IEnumerable. Не смотря на то, что такой код довольно прост, он имеет один очевидных недостаток: в нем происходит итерирование коллекции 10 раз, в то время как тот же результат может быть достигнут простым обращением по индексу.
Решение очевидно — использовать IReadOnlyList:
public int GetTheTenthElement(IReadOnlyList collection)
Нет никакой причины продолжать использовать IEnumerable интерфейс в местах, где вы ожидаете, что коллекция является исчислимой (а вы ожидаете этого в большинстве случаев). Интерфейсы IReadOnlyCollection и IReadOnlyList, добавленные в .Net 4.5, делают эту работу намного проще.
Имплементации IEnumerable и LSP
Что насчет имплементаций IEnumerable, которые нарушают LSP? Давайте взглянем на пример, в котором нижележащим типом IEnumerable является DbQuery. Мы можем получить его следующим образом:
private IEnumerable FindByName(string name) < using (MyContext db = new MyContext()) < return db.Orders.Where(x =>x.Name == name); > >
В этом коде есть очевидная проблема: обращение к БД отложено до тех пор, пока клиентский код не начнет итерировать получившееся множество. Т.к. к этому моменту подключение к БД закрыто, обращение приведет к исключению:
public void Process(IEnumerable orders) < foreach (Order order in orders) // Exception: DB connection is closed < >>
Такая имплементация нарушает LSP, т.к. интерфейс IEnumerable сам по себе не имеет никаких предусловий, требующих наличия открытого подключения к базе данных. Следуя этому интерфейсу, вы должны иметь возможность проитерировать IEnumerable не зависимо от того, существует ли такое подключение. Как мы видим, класс DbQuery усилил предусловия IEnumerable и, таким образом, нарушил LSP.
В общем-то, это не обязательно является признаком плохого дизайна. Ленивые вычисления — довольно распространенный подход при работе с БД. Он позволяет выполнять несколько запросов за одно обращение к БД и таким образом увеличивает общую производительность системы. Ценой здесь является нарушение LSP принципа.
Что такое IEnumerable и IEnumerator в C#? [закрыт]
Хотите улучшить этот вопрос? Переформулируйте вопрос так, чтобы он был сосредоточен только на одной проблеме.
Закрыт 2 года назад .
Что такое IEnumerable и IEnumerator в C#? Я много искал в интернете информации об этом, но везде объясняется слишком научно и непонятно. Можете объяснить, как это работает, как реализуется и, собственно, зачем это нужно так, чтобы было понятно и, если получится, с примерами?
Отслеживать
задан 24 июл 2021 в 19:53
Евгений Голосов Евгений Голосов
27 1 1 серебряный знак 2 2 бронзовых знака
везде объясняется слишком научно и непонятно . Можете объяснить . так, чтобы было понятно нет, не можем. Пишите конкретный вопрос, получите конкретный ответ. Мы не в курсе вообще, что для вас сложно, а что нет. А тратить время на разъяснения, чтобы потом вы и тут скащали, что сложно сильно — никому не надо.
24 июл 2021 в 20:14
я поставил конкретный вопросы: как это работает, как реализуется, зачем это нужно
24 июл 2021 в 20:26
Если совсем просто, то это 2 интерфейса 🙂
– user206435
25 июл 2021 в 9:43
как это работает, как реализуется, зачем это нужно — и чтобы было написано не так как везде, а то там слишком научно и непонятно — это не конкретный вопрос. C# зарелизился 19 лет назад. если все, что написано с тех пор про базовый интерфейс IEnumerable для всех возможных уровней подготовки, включая детей, для вас «слишком научно» — значит вам стоит напрячься и сформулировать вопрос более точно, а то участники в очередной раз ответят вам «слишком научно». может вы не понимаете что такое интерфейс, или что такое коллекция — и все эти подробности в ответах на два экрана — просто впустую
– user177221
25 июл 2021 в 19:38
@PashaPash, а по-моему вопросы «Как это работает?», «Где это используется?» и «Как реализуется?» являются конкретными. Да, возможно я не совсем правильно сформулировал и попросил объяснить более понятно. Но все же пользователи смогли понятно ответить и донести информацию.С другой стороны, если бы в документациях все было «для всех возможных уровней подготовки, включая детей», то люди не задавали бы вопросов на подобии таких, как задал я, и не снимали бы различные видео для упрощения понимания тем
25 июл 2021 в 21:53
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
У многих современных языков есть понятие «автоматического обхода коллекции». Например, у C++ есть кроме обычного цикла с индексами также и цикл for (T variable : collection) , который обходит в том числе и неиндексируемые коллекции. У Java есть такой же цикл.
Ценность такого цикла состоит в том, что вы можете «обходить» любую коллекцию, не заботясь о том, как же это сделать: например, для массива это просто обход по индексу, для структур, основанных на дереве — это обход дерева и. т. д.
Можно было бы захардкодировать методы обхода известных коллекций, но это слабо расширяемое решение, т. к. оно не позволяет создавать кастомные, ваши коллекции. А также компилятор приходилось бы обновлять каждый раз, когда стандартная библиотека добавляла бы в себя новую коллекцию. Поэтому такая идея не проходит, и нужно придумать общее решение, не зависящее от конкретной коллекции.
Итак, нам нужна абстракция, которая говорит «объекты этого типа представляют собой набор элементов, который можно обходить циклом наподобие for ». В OOP-языках такого рода абстракции традиционно представляются в виде интерфейсов. Поэтому и появился интерфейс IEnumerable (где T — тип элемента коллекции), означающий перечислимый контейнер (то есть тот, который можно обойти циклом).
Было решено, что для обхода будет применяться следующая языковая конструкция:
foreach (T current in collection) < // обработать элемент current >
Как можно это реализовать? Язык может опираться лишь на интерфейс IEnumerable , значит, нужные методы должны быть «зашиты» в интерфейс IEnumerable . Поскольку коллекцию могут обходить одновременно несколько циклов (например, так:
foreach (int x in collection) foreach (int y in collection) Console.WriteLine("(, )");
вполне можно написать, а ещё можно одновременно обходить коллекцию из разных потоков), то нам нужно знать, в каком месте находится каждый из одновременных обходов коллекции.
Это самое состояние обхода должно быть как-то представлено, и за это, как было решено, отвечает интерфейс IEnumerator . Он содерждит для массива — текущий индекс в данном обходе, для дерева — указатель на текущий узел дерева, для более сложных случаев — более сложные вещи. Каждая коллекция «придумывает» себе подходящий IEnumerator . Интерфейс IEnumerable , таким образом, получается очень простым: он содержит лишь функцию GetEnumerator() , которая возвращает энумератор, который представляет данные для конкретного обхода. Цикл foreach должен получить энумератор, и дальше работать только с ним.
Теперь, энумератор. Он отвечает за один конкретный обход, и ему достаточно содержать две функции: MoveNext() для перехода к следующему элементу, и Current для получения текущего элемента.
Таким образом, энумерация выглядит приблизительно так:
var enumerator = collection.GetEnumerator(); while (enumerator.MoveNext()) < var current = enumerator.Current; // обработать элемент current >
Вот во что под капотом превращается цикл foreach .
Вот таким образом интерфейсы IEnumerable и IEnumerator позволяют реализовать энумерацию произвольного класса. Вот вам простой пример реализации (я убрал все проверки, чтобы код был проще):
class CustomEnumerable : IEnumerable < readonly int[] data; // просто массив, который можно обходить public CustomEnumerable(int[] data) =>this.data = data; // отдаём сконструированный нами объект public IEnumerator GetEnumerator() => new EnumeratorImplementation(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); // этот класс отвечает за одну энумерацию объекта CustomEnumerable class EnumeratorImplementation : IEnumerator < readonly CustomEnumerable self; // ссылка на сам объект public EnumeratorImplementation(CustomEnumerable self) =>this.self = self; int currIndex = -1; // текущий индекс в энумерации bool isIndexValid => currIndex >= 0 && currIndex < self.data.Length; public int Current =>isIndexValid ? self.data[currIndex] : 0; object IEnumerator.Current => Current; public bool MoveNext() // переход к следующему: < // если мы уже прошли до конца, больше увеличивать индекс не надо if (currIndex < self.data.Length) currIndex++; return isIndexValid; // true если мы ещё не дошли до конца >public void Reset() => currIndex = -1; public void Dispose() < >> >
Ienumerable c что это
![]()

Поиск
Интерфейс IEnumerable. Самый базовый интерфейс для коллекций в C#
Посмотрели 12269 раз(а)
Интерфейсы: IEnumerable, IEnumerator в C#
последнее обновление: 13 сентября 2018
IEnumerable
Предоставляет перечислитель.
Для IEnumerable доступен оператор foreach
Определено в пространстве имен
System.Collections
Синтаксис
interface IEnumerable
![]()
IEnumerator GetEnumerator ()
возвращает перечислитель, который осуществляет итерацию по коллекции
IEnumerator
Поддерживает простой перебор элементов.
Определено в пространстве имен
System.Collections
Синтаксис
interface IEnumerator
Свойства
![]()
object Current < get; >
получает текущий элемент в коллекции
Методы
![]()
bool MoveNext ()
перемещает перечислитель к следующему элементу коллекции
![]()
void Reset ()
устанавливает перечислитель в его начальное положение, т. е. перед первым элементом коллекции
C# Создаем новое C# консольное приложение. и напишем код
using System;
using System.Collections.Generic;
namespace ConsoleApplication1
<
class Program
<
static void Main( string [] args)
<
// заполняем
IEnumerable values = new int [] < 5, 8, 12, 6 >;
// проходим по IEnumerable
foreach (object v in values)
<
// показываем значение на экране
Console .WriteLine(v);
>
>
>
>
Результат примера

Где используется IEnumerable
Необобщенные коллекции в C# . наследуются от IEnumerable

класс для добавления, удаления элементов
класс хранящей наборы пар «ключ-значение», отсортированных по ключу
класс использует алгоритм «последний вошел — первый вышел».
Каждый добавленный элемент помещается в самый верх.
Первым извлекается элемент, который находится в самом верху стека.
класс использует алгоритм «первый вошел — первый вышел».
тоже самое что и очередь в магазине
класс храняющий набор пар «ключ-значение»
класс хранящий массив битовых значений
← Предыдущая тема
Что такое обобщенные (типизированные) коллекции в C# ? Классы List, SortedList, Stack, Dictionary, LinkedList, Queue, HashSet, SortedSet, ConcurrentDictionary, SortedDictionary