5.05. Обобщения
Обобщения
Обобщения (generics) — это механизм языка C#, позволяющий определять классы, методы, интерфейсы, делегаты и структуры без указания конкретного типа данных. Вместо этого используется параметр типа, который заменяется на реальный тип во время использования.
List<int> numbers = new List<int>();
Dictionary<string, Person> people = new Dictionary<string, Person>();
Здесь int, string, Person — конкретные типы, подставляемые вместо параметров T, TKey, TValue.
Обобщённый тип — это тип (класс, структура, интерфейс и т.д.), который объявлен с одним или несколькими параметрами типа. Эти параметры — «заглушки», которые будут заменены на реальные типы при создании экземпляра.
public class Box<T>
{
public T Content { get; set; }
}
Здесь T — параметр типа. Когда ты создаёшь Box<int>, T становится int.
Зачем это нужно?
Во-первых, типобезопасность - нет приведений типов (cast), нет ошибок времени выполнения из-за невыполнимых типов.
Во-вторых, производительность, для значимых типов нет упаковки-распаковки, что критично для производительности.
В-третьих, повторное использование кода - один класс List<T> работает с int, string, Person, DateTime и любыми другими типами.
При создании библиотек, которые должны работать с любыми типами, при работе с коллекциями, где нужна типобезопасность и производительность, при реализации алгоритмов, независимых от конкретного типа данных используются обобщенные типы.
Примеры:
List<T> // Список элементов любого типа T
Dictionary<K, V> // Словарь с ключами K и значениями V
Stack<T> // Стек из элементов типа T
Параметры типа — это идентификаторы, используемые как «переменные для типов». Хотя можно использовать любые имена, существуют общепринятые соглашения:
Буквы эти не имеют значения сами по себе, это просто соглашения:
- T – Type (тип – любой);
- K – Key (ключ в словаре);
- V – Value (значение в словаре);
- U, S – дополнительные типы, если нужно несколько (второй и третий произвольный тип);
- TResult – результат метода или функции;
- TKey и TValue – то же самое, что и K и V.
Соответственно, можно использовать любые имена:
Dictionary<TKey, TValue>
KeyValuePair<TKey, TValue>
Func<T, TResult>
Action<T1, T2>
public class Box<Apple> { ... }
Пример:
List<int> numbers = new List<int>();
numbers.Add(10);
int x = numbers[0]; // Нет необходимости приводить к int
То же самое можно сделать со string, Person, DateTime и т.д.:
List<string> names = new List<string>();
names.Add("Alice");
string name = names[0];
Вместо того, чтобы писать отдельный класс ListOfInt, ListOfString, ListOfPerson и так далее – мы используем один универсальный класс List<T>.
Обобщённые классы.
Обобщённый класс — это класс, принимающий тип в качестве параметра.
public class Box<T>
{
public T Item { get; set; }
}
Пример – создаём класс Box<T>:
public class Box<T>
{
public T Content { get; set; }
public void Show()
{
Console.WriteLine(Content);
}
}
Использование:
Box<int> box1 = new Box<int>();
box1.Content = 42;
box1.Show(); // вывод: 42
Box<string> box2 = new Box<string>();
box2.Content = "Hello";
box2.Show(); // вывод: Hello
Каждый экземпляр — типобезопасен. Нельзя положить string в Box<int>.
Обобщённые методы.
Методы тоже могут быть обобщёнными — они определяют свои параметры типа.
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Пример – создаём метод T Max<T>(T a, T b) where T : IComparable<T>:
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Использование:
int maxInt = Max(10, 20); // 20
string maxStr = Max("apple", "banana"); // banana
Здесь компилятор сам определяет тип T. Компилятор выводит тип автоматически — не нужно писать Max<int>(5, 10).
Без where T : IComparable<T> — CompareTo не будет доступен.
Иногда обобщённому коду нужно знать что-то о типе T, чтобы вызывать методы, создавать экземпляры и т.д. Для этого используются ограничения (where).
Чтобы обобщённый код мог выполнять операции над типом T, нужно иногда указывать ограничения. Ограничения типов указывают, какие типы могут использоваться в качестве аргумента:
where T : class– T должен быть ссылочным типом;where T : struct– T должен быть значимым типом;where T : SomeBaseClass– T должен наследоваться от SomeBaseClass;where T : ISomeInterface– T должен реализовывать интерфейс ISomeInterface;where T : new()– должен иметь конструктор без параметров;where T : unmanaged- неуправляемый тип (без сборки мусора);where T : notnull- не может быть null;where T : U- должен быть совместим с U.
Пример: класс с ограничениями
public class Service<T> where T : class, new()
{
public T CreateInstance()
{
return new T(); // Работает благодаря new()
}
}
Без new() нельзя писать new T(). Чтобы правильно использовано нужно добавить именно where T: new().