JavaRush /Курсы /C# SELF /Роль интерфейсов в коллекциях C#

Роль интерфейсов в коллекциях C#

C# SELF
28 уровень , 0 лекция
Открыта

1. Зачем коллекциям интерфейсы?

Давайте честно: пока мы писали код с конкретным типом (List<int>, Dictionary<string, string>), всё было просто и понятно. Но представьте, что вы вдруг решили заменить List<T> на HashSet<T>, потому что вам понадобились только уникальные значения. Или создать метод, который мог бы работать и с одним, и с другим типом коллекций. Как быть?

Сделать единую входную точку — нужен «универсальный разъём». В C# таким разъёмом выступают интерфейсы. Они определяют набор правил (методов, свойств), которыми должна обладать коллекция. Всё, что реализует интерфейс — поддерживает указанные операции.

Это как зарядка для телефона: если есть стандартный разъем USB-C, вы сможете заряжать и телефон, и современный пылесос, и даже электрическую зубную щетку (главное — чтобы не спутать их в тёмной комнате).

Пример на практике

Допустим, вы хотите написать метод, который считает сумму всех чисел в любой коллекции чисел. Вот как это выглядело бы без интерфейсов:


// Такой метод работает только с массивами:
static int SumArray(int[] array)
{
    int sum = 0;
    for (int i = 0; i < array.Length; i++)
    {
        sum += array[i];
    }
    return sum;
} 

// А вот такой - только с List<int>:
static int SumList(List<int> list)
{
    int sum = 0;
    for (int i = 0; i < list.Count; i++)
        sum += list[i];
    return sum;
}

Неудобно! Ведь код-то очень похожий. С помощью интерфейса IEnumerable<int> можно сделать универсально:


static int SumAll(IEnumerable<int> collection)
{
    int sum = 0;
    foreach (var number in collection)
        sum += number;
    return sum;
}

И теперь этот метод принимает любой тип коллекции, который реализует IEnumerable<int>: массив, список, множество и так далее.

2. Основные интерфейсы коллекций

В .NET для коллекций выделяют несколько ключевых интерфейсов. Давайте рассмотрим их коротко — в дальнейших лекциях вы узнаете детали реализации каждого.

Интерфейс Для чего нужен Пример коллекций, реализующих интерфейс
IEnumerable<T>
Перечисление элементов (foreach) Все:
List<T>
,
HashSet<T>
, массивы
ICollection<T>
Изменяемый набор (Add, Remove, Count)
List<T>
,
HashSet<T>
,
Dictionary<K,V>
IList<T>
Доступ по индексу, изменение
List<T>
, массивы
IDictionary<K,V>
Работа с парами "ключ-значение"
Dictionary<K,V>
,
SortedDictionary<K,V>
ISet<T>
Множество (уникальные элементы)
HashSet<T>
,
SortedSet<T>

P.S. Для простоты мы пока что опускаем "несколько уровней" наследования между этими интерфейсами. Кто от кого наследует — об этом чуть позже.

Визуализация: дерево интерфейсов коллекций


           IEnumerable<T>
                 ^
                 |
          ICollection<T>
              ^       ^
              |       |
         IList<T>  ISet<T>
              |       |
         List<T>   HashSet<T>
Иерархия интерфейсов для коллекций-списков и множеств

Для словарей:


           IEnumerable<KeyValuePair<K,V>>
                      ^
                      |
               ICollection<KeyValuePair<K,V>>
                      ^
                      |
               IDictionary<K,V>
                      |
                Dictionary<K,V>
Иерархия интерфейсов для словарей

3. Зачем интерфейсы в реальной жизни?

Гибкость

Когда ваш метод или класс оперирует не конкретным типом (List<int>), а интерфейсом (IEnumerable<int>), вы можете передавать ему любые коллекции, которые реализуют этот интерфейс. В итоге программа становится гибкой и расширяемой. (Вместо «Только iPhone» — теперь подходит любой телефон с Bluetooth.)


static void PrintAll(IEnumerable<string> collection)
{
    foreach (var item in collection)
        Console.WriteLine(item);
}

Теперь можно печатать элементы любого списка, массива, множества, даже результата LINQ-запроса!

Унификация

Если вы реализуете свой собственный тип коллекции (например, какую-то очень специфическую коллекцию для какой-то задачи), реализуйте нужные интерфейсы — и ваша коллекция будет работать с чужим кодом, который о вашей коллекции не знает. Вот почему в .NET часто пишут: «программируйте на уровне интерфейсов, а не конкретных реализаций».

Совместимость со стандартными инструментами

Благодаря поддержке стандартных интерфейсов, ваши коллекции могут работать с LINQ, с сортировкой, с сериализацией и другими возможностями .NET.

4. List<T> — это и список, и коллекция, и перечисление

Пара слов о перечислении

IEnumerable<T> — это самый главный и самый базовый интерфейс коллекций в .NET. Он определяет один-единственный метод — GetEnumerator(), который возвращает объект-перечислитель (Enumerator) для итерации по коллекции. Именно благодаря ему вы можете писать:


foreach (var name in names)
{
    Console.WriteLine(name);
}

Где names — это и список, и множество, и даже массив. foreach — это хитрый синтаксический сахар, который под капотом использует метод GetEnumerator().

Интерфейс IEnumerable (без <T>) — более старый, необобщённый. Сейчас почти всегда используйте обобщенную версию.

Синтаксис объявления класса List

Посмотрим на объявление класса List<T>, если немного приоткрыть его магию:


public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...

Это значит, что объект List<T> можно использовать как:

  • Список с произвольным доступом по индексу (IList<T>)
  • Коллекцию с возможностью добавлять/удалять элементы (ICollection<T>)
  • Просто набор элементов, по которому можно пройтись (IEnumerable<T>)

Пример:


List<string> names = new List<string> { "Анна", "Борис", "Вася" };

// Как IEnumerable - можно пройти в цикле
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);

// Как ICollection - можно узнать Count:
ICollection<string> asCollection = names;
int count = asCollection.Count;

// Как IList - можно обратиться по индексу:
IList<string> asList = names;
string firstItem = asList[0];

5. Пример приложения

Допустим, у нас есть простое приложение учета пользователей и их ролей (мы начали его делать в прошлых лекциях):


// Класс пользователя
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Мы можем хранить пользователей в любой коллекции, реализующей IEnumerable<User>:


List<User> listUsers = new List<User> { new User { Name = "Анна", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "Борис", Age = 25 } };

// Метод, который выводит имена всех пользователей
static void PrintUserNames(IEnumerable<User> users)
{
    foreach (var user in users)
        Console.WriteLine(user.Name);
}

// Используем с разными типами коллекций:
PrintUserNames(listUsers);
PrintUserNames(setUsers);

Теперь приложение становится гораздо гибче — без магии и кастомных перегрузок!

6. Типичные ошибки и подводные камни

Одна из самых частых ошибок — использование слишком конкретных типов, когда достаточно интерфейса:


// Плохо (жёстко привязались к реализации)
void DoSomethingWithList(List<int> numbers) { ... }

// Лучше (метод работает с любой коллекцией)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }

Ещё одна ловушка — забыть, что интерфейс задает только те операции, которые явно описаны в нём. Например, в IEnumerable<T> нельзя узнать количество элементов (Count), для этого вам нужно использовать ICollection<T> — либо вручную их пересчитать.

2
Задача
C# SELF, 28 уровень, 0 лекция
Недоступна
Реализация интерфейса IEnumerable<T> для собственной коллекции
Реализация интерфейса IEnumerable<T> для собственной коллекции
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
4 декабря 2025
прикольно но громоздко

IEnumerable<KeyValuePair<string, string>> dict = new Dictionary<string, string>();
ICollection<KeyValuePair<string, string>> dict2 = new Dictionary<string, string>();
((Dictionary<string, string>) dict).Add(userInput, userInput);
dict2.Add(KeyValuePair.Create(userInput, userInput));