Generics в C#: зачем нужны и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в 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 должен наследоваться от BaseClass
  • where 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                

Блоги

Рекомендаций по книгам