Интерфейсы коллекций: IEnumerable, ICollection, IList, IReadOnly
↑ 1.5 Коллекции и основы LINQ · ← Предыдущая · → Следующая
Зачем это на собесе
Что принимать и что возвращать из методов:
IEnumerable<T>,IReadOnlyList<T>,List<T>? Это вопрос про дизайн API и про отложенное выполнение. Заодно проверяют, понимаете ли вы, что скрывается заforeachиyield.
Объяснение
Иерархия интерфейсов
| Интерфейс | Что добавляет | Изменяемая |
|---|---|---|
IEnumerable<T> | перебор (foreach) | — |
IReadOnlyCollection<T> | Count | нет |
IReadOnlyList<T> | индексатор [i] | нет |
ICollection<T> | Count, Add, Remove, Clear, Contains | да |
IList<T> | индексатор, Insert, RemoveAt | да |
ISet<T> | операции над множествами (HashSet<T>) | да |
IReadOnlyDictionary<K,V> / IDictionary<K,V> | доступ по ключу | нет / да |
Все они наследуют IEnumerable<T>: IReadOnlyList<T> строится на IReadOnlyCollection<T>, IList<T> на ICollection<T>.
IEnumerable<T>
Минимальный контракт «последовательность»: GetEnumerator() возвращает перечислитель с MoveNext() и Current. foreach разворачивается именно в него.
foreach (var p in patients) { }
// эквивалентно:
using var e = patients.GetEnumerator();
while (e.MoveNext()) { var p = e.Current; }- Последовательность может быть ленивой и даже бесконечной: элементы вычисляются по мере перебора.
- Многократное перечисление ленивой последовательности повторяет вычисление (запрос в БД, чтение файла) заново.
IEnumerable<int> Evens() // итератор: yield
{
for (var i = 0; ; i += 2) yield return i;
}
var firstTen = Evens().Take(10).ToList();ICollection<T> и IList<T>
Изменяемые коллекции: добавление, удаление, Count. IList<T> добавляет доступ по индексу. Реализуют List<T>, массив (частично), Collection<T>.
IReadOnlyCollection<T> и IReadOnlyList<T>
Только чтение: Count и индексатор, без методов изменения. Не гарантируют неизменяемость самих данных: под интерфейсом может быть обычный List<T>, который кто-то изменит.
public sealed class Patient
{
private readonly List<Encounter> _encounters = [];
public IReadOnlyList<Encounter> Encounters => _encounters; // наружу — только чтение
}Для настоящей неизменяемости — ImmutableArray<T>, ImmutableList<T>, FrozenSet<T>/FrozenDictionary<K,V> (.NET 8).
Что принимать и что возвращать
- Параметры: самый общий подходящий тип. Нужен только перебор —
IEnumerable<T>(илиIReadOnlyCollection<T>, если нуженCount). Нужен доступ по индексу —IReadOnlyList<T>. - Возвращаемые значения: достаточно конкретный тип, чтобы вызывающий не пересчитывал. Готовый результат —
IReadOnlyList<T>(илиT[],List<T>для внутренних API). Ленивый поток —IEnumerable<T>с явной документацией. - Не отдавайте наружу внутренний
List<T>и не принимайтеList<T>, если хватает интерфейса.
public IReadOnlyList<PatientDto> GetAll() => _repo.Query().Select(Map).ToList(); // материализован
public IEnumerable<PatientDto> Stream() => _repo.Query().Select(Map); // ленивый, ответственность на вызывающемIQueryable<T>
Наследует IEnumerable<T>, но хранит выражение запроса (дерево Expression), которое провайдер (EF Core) транслирует в SQL. Пока не вызван ToList/First/foreach, запрос не выполняется. Смешение IQueryable и IEnumerable определяет, где выполнится фильтрация: в БД или в памяти.
Ковариантность
IEnumerable<out T> и IReadOnlyList<out T> — ковариантны: IEnumerable<Dog> присваивается IEnumerable<Animal>. List<T> и IList<T> — инвариантны: List<Dog> нельзя присвоить List<Animal>, иначе можно было бы добавить туда Cat.
Нюансы и подводные камни
- Возврат
IEnumerable<T>из метода, который внутри держитusing/соединение: перебор случится после закрытия ресурса. Материализуйте перед выходом или используйтеyieldвнутриusingосознанно. IEnumerable<T>.Count()перебирает всё, если нельзя определить размер быстро (LINQ проверяетICollection<T>/IReadOnlyCollection<T>, но не всегда).- Двойной перебор
IEnumerableс запросом к БД или дорогим вычислением: выполнится дважды. Анализатор предупреждает (CA1851). IReadOnlyList<T>надList<T>— «представление», не копия: изменения отражаются.- Приведение
IEnumerable<T>кList<T>через(List<T>)работает, только если внутри действительноList<T>; используйтеToList().
Практика
- Переписать метод, принимающий
List<int>, чтобы он принималIEnumerable<int>и не перечислял дважды. - Написать итератор с
yield, который читает большой файл построчно. - Показать разницу между ленивым и материализованным результатом на счётчике вызовов.
Вопросы с ответами
Чем
IEnumerable<T>отличается отICollection<T>иIList<T>?
IEnumerable<T>только перебор.ICollection<T>добавляетCountи изменение (Add,Remove).IList<T>дополнительно даёт доступ по индексу и вставку по позиции.
Что вернуть из метода:
List<T>,IEnumerable<T>илиIReadOnlyList<T>?Для готового результата
IReadOnlyList<T>: вызывающий видит размер и индексы, но не может изменить внутреннее состояние.IEnumerable<T>— если поток ленивый.
Что такое отложенное выполнение и чем оно опасно?
Вычисление происходит при переборе, а не при создании запроса. Опасности: повторный перебор пересчитывает, данные могут измениться между созданием и перебором, ресурс (соединение) может быть уже закрыт.
Чем
IQueryable<T>отличается отIEnumerable<T>?
IQueryableхранит выражение и транслируется провайдером (например, в SQL),IEnumerable— делегаты, выполняемые в памяти.
Почему
List<Dog>нельзя присвоитьList<Animal>, аIEnumerable<Dog>вIEnumerable<Animal>можно?
List<T>изменяем и потому инвариантен (иначе можно было бы добавить Cat).IEnumerable<out T>только отдаёт элементы, поэтому ковариантен.
Связанные темы
- Предыдущая: Массивы, List, Dictionary, HashSet, Queue, Stack · Следующая: Generics: обобщённые типы и методы
- LINQ: LINQ: основные операторы
- Ковариантность и generics глубоко: Generics глубоко: constraints, вариантность, static abstract
- IQueryable и EF Core: 4.2 Entity Framework Core