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

Ответ

Эти интерфейсы образуют иерархию, где каждый последующий добавляет новую функциональность к предыдущему. Выбор интерфейса для возврата из метода или использования в 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>).

Ответ 18+ 🔞

Слушай, вот есть у нас в C# эти интерфейсы коллекций, да? Так вот, они как матрёшки, одна в другой, только каждая следующая — это как предыдущая, но с новыми прибамбасами. И если ты возвращаешь из метода не ту матрёшку, можешь нарваться на проблемы, потому что обещаешь одно, а даёшь другое.

Вот как они друг в друга вкладываются: IEnumerable<T>ICollection<T>IList<T>

1. IEnumerable<T> — Самая маленькая матрёшка, просто перечисление

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

2. ICollection<T> — Матрёшка побольше, уже коллекция

  • Что она делает: Всё, что умеет IEnumerable<T>, плюс может коллекцию менять.
  • Добавляет: Добавление, удаление, очистку, проверку на наличие элемента и, о чудо, свойство Count!
  • Что можно: Всё от перечисления, плюс Add, Remove, Clear, Contains. Узнать точное количество элементов — раз плюнуть.
  • Что НЕЛЬЗЯ: Спросить «дай мне элемент номер три». Индекса нет, забудь.
  • Кто так умеет: 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> — Самая большая и крутая матрёшка, список

  • Что она может: Всё, что умеет 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, SortedDictionary). С ними та же история, только логика своя.