В чем разница между основными интерфейсами коллекций в C# (IEnumerable, ICollection, IList)?

«В чем разница между основными интерфейсами коллекций в C# (IEnumerable, ICollection, IList)?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Эти интерфейсы образуют иерархию, где каждый последующий добавляет новую функциональность к предыдущему. Выбор интерфейса для возврата из метода или использования в API определяет контракт на возможности работы с коллекцией.

Иерархия: IEnumerable<T>ICollection<T>IList<T>

1. IEnumerable<T> — Перечисление (Enumeration)

  • Минимальный контракт: «Я могу отдавать элементы по одному».
  • Ключевой метод: GetEnumerator().
  • Что позволяет: Только последовательное чтение с помощью foreach или LINQ.
  • Не позволяет: Проверять количество элементов, добавлять, удалять, обращаться по индексу.
  • Идеален для возврата из методов, где нужно только прочитать данные, или для приема параметров, которые нужно только перебрать.
// Метод принимает минимальный интерфейс — только для чтения.
public void ProcessItems(IEnumerable<Product> products)
{
    foreach (var product in products) // Единственная гарантированная операция.
    {
        Console.WriteLine(product.Name);
    }
    // products.Add(...) // НЕ КОМПИЛИРУЕТСЯ!
}

2. ICollection<T> — Коллекция (Collection)

  • Расширяет IEnumerable<T>.
  • Добавляет: Модификацию коллекции (добавление, удаление, очистка) и свойство Count.
  • Что позволяет: Всё, что IEnumerable<T>, плюс Add, Remove, Clear, Contains, CopyTo и получение точного количества элементов.
  • Не позволяет: Обращаться к элементам по индексу.
  • Представлен такими типами, как HashSet<T>, LinkedList<T>, Queue<T>, Stack<T>.
public void FillCollection(ICollection<string> bag)
{
    bag.Add("item1"); // Можно модифицировать.
    bag.Add("item2");
    Console.WriteLine($"Added {bag.Count} items"); // Можно узнать количество.
    // var x = bag[0]; // НЕ КОМПИЛИРУЕТСЯ! Нет индексатора.
}

3. IList<T> — Список (List)

  • Расширяет ICollection<T>.
  • Добавляет: Прямой доступ по индексу (чтение и запись) и методы для работы с индексами (Insert, RemoveAt, IndexOf).
  • Что позволяет: Всё, что ICollection<T>, плюс list[0], Insert(0, item), RemoveAt(0).
  • Гарантирует порядок элементов.
  • Основная реализация: List<T>, массивы (T[]).
public void ReorderList(IList<int> numbers)
{
    if (numbers.Count > 1)
    {
        // Полный доступ для чтения и записи по индексу.
        var temp = numbers[0];
        numbers[0] = numbers[^1]; // numbers[numbers.Count - 1]
        numbers[^1] = temp;
    }
}

Правило выбора:

  • Возвращайте или принимайте IEnumerable<T>, если нужен только перебор (максильная гибкость для вызывающего кода).
  • Используйте ICollection<T>, если ваш метод должен добавлять/удалять элементы в переданную коллекцию, но ему не важен индекс.
  • Используйте IList<T>, если критически важен прямой доступ или изменение по индексу.

Отдельно стоит IDictionary<TKey, TValue> — это параллельная ветвь для коллекций пар «ключ-значение» (Dictionary<TKey, TValue>, SortedDictionary<TKey, TValue>).