Возможно, вы слышали о массивах и контейнерах - объектах, в которых хранится список других объектов.
Но для того, чтобы объект представлял список, ему фактически не нужно «хранить» список. Все, что он должен сделать, это предоставить вам методы или свойства, которые позволяют вам получать элементы списка.
В .NET Framework интерфейс IEnumerable - это все, что должен поддерживать объект, чтобы его можно было считать "списком" в этом смысле.
Чтобы немного упростить это (исключая некоторый исторический багаж):
public interface IEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}
Так что вы можете получить из него счетчик. Этот интерфейс (опять же, слегка упрощая для устранения отвлекающего шума):
public interface IEnumerator<T>
{
bool MoveNext();
T Current { get; }
}
Итак, чтобы пройтись по списку, вы должны сделать следующее:
var e = list.GetEnumerator();
while (e.MoveNext())
{
var item = e.Current;
// blah
}
Этот шаблон аккуратно фиксируется ключевым словом foreach
:
foreach (var item in list)
// blah
А как насчет создания нового вида списка? Да, мы можем просто использовать List<T>
и заполнить его предметами. Но что, если мы хотим обнаружить предметы «на лету», как они запрашиваются? В этом есть преимущество, заключающееся в том, что клиент может отказаться от итерации после первых трех элементов, и ему не нужно «оплачивать стоимость» генерации всего списка.
Реализация такого рода ленивого списка вручную была бы хлопотной. Нам нужно написать два класса, один для представления списка путем реализации IEnumerable<T>
, а другой для представления активной операции перечисления путем реализации IEnumerator<T>
.
Методы итератора делают всю тяжелую работу за нас. Мы просто пишем:
IEnumerable<int> GetNumbers(int stop)
{
for (int n = 0; n < stop; n++)
yield return n;
}
И компилятор преобразует это в два класса для нас. Вызов метода эквивалентен созданию объекта класса, представляющего список.