1. Краткая предыстория
В древние времена в C# было всего две радости при работе с коллекциями: использовать массивы или так называемые "неглубоко типизированные" коллекции — вроде ArrayList, где можно было складывать объекты любого типа. Казалось бы, свобода! Но стоило добавить в такую коллекцию строку и целое число вперемешку — и начинался балаган: нельзя безопасно извлечь элемент без дополнительных (и не всегда срабатывающих) проверок типа. А ещё — жуткие ошибки на ровном месте и танцы с кастингом (преобразованием типов).
Пример не-generic коллекции (ArrayList):
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("Hello C#");
int number = (int)stuff[0]; // ОК
string text = (string)stuff[1]; // ОК
int fail = (int)stuff[1]; // БУМ! InvalidCastException (runtime error)
Да, компилятор не ругается — ошибка всплывает только во время выполнения! Это как открыть холодильник и обнаружить там горячую кастрюлю — сюрприз!
В чём суть обобщения?
Обобщённые коллекции (Generics) появились, чтобы можно было создавать коллекции с гарантированным типом содержимого. Это даёт три ключевых преимущества:
- Безопасность типов на этапе компиляции. Компилятор не даст вам случайно засунуть в коллекцию что-то лишнее.
- Удобство: нет необходимости каждый раз приводить тип вручную.
- Производительность: не тратится время на ненужную "упаковку и распаковку" значимых типов (boxing/unboxing).
2. Что такое Generics (обобщения) вообще?
Generics — это магический способ описывать структуры данных и методы так, чтобы они работали с любым типом, но при этом оставались строго типизированными.
Вообразите универсальную коробку, которую можно использовать как для книг, так и для носков, но только для одного типа за раз. Если коробка объявлена как "только для книг" — никто не сможет подложить туда носки. То же и с generic-коллекциями: если вы создаёте коллекцию из int, туда не попадёт случайно string.
Пример объявления обобщённого класса
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Hello Generics!" };
T — это "параметр типа", который укажет, с чем будет работать ваша коробка. C# потребует, чтобы вы всегда чётко указывали, что за тип кладёте.
Обобщённые коллекции в .NET
В .NET Framework практически все современные коллекции имеют generic-версии. Это:
- List<T> — динамический список элементов типа T.
- Dictionary<TKey, TValue> — ассоциативный массив (словарь) с ключами и значениями.
- Queue<T>, Stack<T> — очереди и стеки.
- и многие другие.
3. Как это работает: устройство Generics
Параметры типа
Когда вы объявляете коллекцию вроде List<int>, компилятор C# создаёт отдельный вариант (specialization) этого класса специально для типа int. Когда же вы объявляете List<string>, компилятор C# создаёт еще один вариант этого класса, но уже для строк и т.п.
А для вас, как программиста, это работает как магия:
List<int> numbers = new List<int>();
numbers.Add(1); // Можно добавить только int
List<string> words = new List<string>();
words.Add("hello"); // Можно добавить только string
При попытке добавить элемент другого типа компилятор тут же выразит недовольство:
numbers.Add("fail"); // ОШИБКА на этапе компиляции!
Безопасность типов: Compile-time vs. Run-time
Это значит, что ошибки, подобные приведённой выше, не дойдут до запускной стадии программы (run-time). Ваша коллекция настолько защищена, что к ней не подкопаться — компилятор стоит на страже, как цербер у дверей.
Под капотом
- Для ссылочных типов (например, string, object) больше никаких лишних приведений типов.
- Для значимых типов (int, double) исчезает проблема "упаковки/распаковки" (boxing/unboxing).
- Использование generics в .NET не приводит к избыточному количеству кода — CLR оптимизирует это на этапе JIT-компиляции.
4. Как generics повышают производительность
Давайте ещё раз подчеркнем те волшебные свойства, которые Generics дарят нашему коду:
Типовая безопасность (Type Safety):
Это, пожалуй, самое важное. Благодаря Generics, компилятор становится вашим личным телохранителем, который не пропустит "чужаков" в вашу коллекцию. Вы можете быть абсолютно уверены, что List<Product> будет содержать только объекты Product, а не какие-нибудь случайные строки или числа. Это исключает целый класс ошибок, которые раньше могли проявляться только во время работы программы и приводить к неожиданным "взрывам" (InvalidCastException). Ваш код становится более надежным и предсказуемым.
Производительность (Performance):
Как мы уже говорили, для значимых типов (вроде int, double или DateTime) Generics позволяют более эффективно работать с памятью и процессорным временем. Для коллекций, которые хранят миллионы элементов или часто модифицируются, это может быть критически важным фактором. Вместо того чтобы постоянно перекладывать апельсины из ящика в мешок и обратно, вы просто кладете их прямо в нужный ящик.
Повторное использование кода (Code Reusability):
Generics позволяют вам писать один и тот же код, который будет работать с различными типами данных без необходимости его дублирования. Допустим, вам нужна функция для обмена местами двух переменных. Без Generics вам бы пришлось писать SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduct(ref Product a, ref Product b) и так далее. С Generics вы пишете всего одну функцию: Swap<T>(ref T a, ref T b). Этот принцип применим и к коллекциям: вам не нужны IntList, StringList, ProductList – достаточно List<T>. Это делает ваш код более компактным, легко поддерживаемым и масштабируемым.
5. Обобщённые методы и собственные generic-классы
Обобщённые методы
Generics — это не только про коллекции! Вы можете писать свои обобщённые методы, которые работают с любым типом. Это делает код более универсальным.
//Указываем тип-параметр T сразу после имени метода
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// Использование:
int a = 10, b = 20;
Swap(ref a, ref b); // Теперь a == 20, b == 10
string one = "один", two = "два";
Swap(ref one, ref two); // Работает и для строк!
Обратите внимание, что C# компилятор может сам определить тип-параметр метода исходя из типа передаваемых переменных. Поэтому в примере выше в метод Swap не нужно дополнительно передавать тип.
Собственные generic-классы
Вы даже можете создавать свои собственные "коробки" (generic-классы). Это проще чем кажется:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// Использование:
var pair = new Pair<int, string> { First = 42, Second = "ответ" };
6. Пример использования generic-коллекций
Давайте вернёмся к нашему "списку задач" (To-Do List), который мы начали строить в примерах предыдущих лекций. Раньше мы хранили задачи в массиве или просто выводили их на экран. Теперь — храним их динамически в List<string>.
Пример: динамическое добавление задач в список
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("Введите задачу (или пустую строку для завершения):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("Задача добавлена! Введите ещё (или пустую строку для завершения):");
}
Console.WriteLine("\nВаш список задач на сегодня:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
Что здесь хорошего?
- Коллекция автоматически расширяется по мере добавления новых задач.
- Нельзя случайно добавить в список что-то кроме строки.
- Можно легко перебирать список и выводить его содержимое.
7. Типичные ошибки начинающих, особенности и советы
Переход с не-generic коллекций на generic порой вызывает замешательство. Вот пара распространённых ситуаций, с которыми сталкиваются новички:
- Попытка добавить элемент не того типа (List<int> numbers = new List<int>(); numbers.Add("hi"); // Ошибка).
- Желание смешивать типы в одной коллекции — тут надо выбирать базовый тип (например, List<object>) — и потом придётся всегда приводить обратно к нужному типу.
- Забывают про существование constraints, пишут слишком "широкие" обобщения и ловят странные ошибки на этапе компиляции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ