Введение в Generics в C#
Generics (обобщенные типы) — одна из мощнейших возможностей языка C#, появившаяся в .NET Framework 2.0. Они позволяют создавать классы, методы и интерфейсы, которые работают с любым типом данных, сохраняя при этом строгую типизацию. В этой статье мы разберем, зачем нужны generics в C#, как их использовать и какие преимущества они дают разработчику.
Представьте, что вам нужно написать метод для обмена значениями двух переменных. Без generics вам пришлось бы создавать отдельные методы для каждого типа: SwapInt, SwapString, SwapDouble и так далее. С generics эта задача решается одной универсальной функцией.
Основные преимущества Generics
1. Безопасность типов на этапе компиляции
Generics обеспечивают проверку типов во время компиляции, а не во время выполнения. Это означает, что ошибки, связанные с несоответствием типов, будут обнаружены на раннем этапе разработки.
// Без generics - возможна ошибка во время выполнения
ArrayList list = new ArrayList();
list.Add(123);
list.Add("строка");
int number = (int)list[1]; // InvalidCastException!
// С generics - ошибка на этапе компиляции
List<int> numbers = new List<int>();
numbers.Add(123);
numbers.Add("строка"); // Ошибка компиляции!
2. Производительность
Использование generics позволяет избежать операций упаковки (boxing) и распаковки (unboxing) для значимых типов. Это значительно повышает производительность при работе с коллекциями.
// Boxing/Unboxing - медленно
ArrayList list = new ArrayList();
list.Add(42); // boxing
int value = (int)list[0]; // unboxing
// Generics - быстро, без boxing
List<int> list = new List<int>();
list.Add(42); // без boxing
int value = list[0]; // без unboxing
3. Переиспользование кода
Один generic-класс или метод может работать с любым типом данных, что избавляет от дублирования кода.
Создание и использование Generics
Generic классы
Рассмотрим пример создания обобщенного класса для хранения пары значений:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public void Display()
{
Console.WriteLine($"First: {First}, Second: {Second}");
}
}
// Использование
var pair1 = new Pair<int, string>(1, "один");
var pair2 = new Pair<string, double>("PI", 3.14159);
pair1.Display(); // First: 1, Second: один
pair2.Display(); // First: PI, Second: 3.14159
Generic методы
Методы также могут быть обобщенными. Вот классический пример — метод обмена значений:
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// Использование
int x = 5, y = 10;
Swap<int>(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}"); // x = 10, y = 5
string s1 = "Hello", s2 = "World";
Swap(ref s1, ref s2); // тип можно не указывать
Console.WriteLine($"s1 = {s1}, s2 = {s2}"); // s1 = World, s2 = Hello
Generic интерфейсы
Интерфейсы с generics позволяют создавать гибкие контракты:
public interface IRepository<T>
{
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class UserRepository : IRepository<User>
{
public User GetById(int id)
{
// Реализация получения пользователя
return new User();
}
public void Add(User user)
{
// Реализация добавления
}
public void Update(User user)
{
// Реализация обновления
}
public void Delete(int id)
{
// Реализация удаления
}
}
Ограничения (Constraints) в Generics
Иногда необходимо ограничить типы, которые могут использоваться с generic. Для этого применяются ограничения (constraints):
Основные типы ограничений
where T : struct— T должен быть значимым типом (int, double, DateTime)where T : class— T должен быть ссылочным типомwhere T : new()— T должен иметь конструктор без параметровwhere T : BaseClass— T должен наследоваться от BaseClasswhere T : IInterface— T должен реализовывать интерфейс IInterface
public class Calculator<T> where T : struct
{
public T Add(T a, T b)
{
// Работает только для значимых типов
return (dynamic)a + (dynamic)b;
}
}
public class Factory<T> where T : new()
{
public T Create()
{
return new T(); // Работает, если есть конструктор без параметров
}
}
public class Repository<T> where T : IEntity
{
public void Save(T entity)
{
entity.Validate();
// Сохранение в БД
}
}
Множественные ограничения
public class AdvancedCollection<T>
where T : class, IComparable<T>, new()
{
public T CreateAndCompare(T item)
{
T newItem = new T();
return newItem.CompareTo(item) > 0 ? newItem : item;
}
}
Практические примеры использования Generics
Generic Singleton
public class Singleton<T> where T : class, new()
{
private static readonly Lazy<T> _instance =
new Lazy<T>(() => new T());
public static T Instance => _instance.Value;
private Singleton() { }
}
// Использование
var config = Singleton<Configuration>.Instance;
Generic репозиторий с кэшированием
public class CachedRepository<T> where T : class
{
private readonly Dictionary<int, T> _cache = new();
private readonly IRepository<T> _repository;
public CachedRepository(IRepository<T> repository)
{
_repository