← Назад к вопросам

Что такое дженерики (Generics) в C#? Какие проблемы они решают?

2.0 Middle🔥 182 комментариев
#C# и ООП

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое дженерики (Generics) в C#?

Дженерики — это мощный механизм языка C#, позволяющий создавать классы, интерфейсы, методы и делегаты, которые работают с произвольными типами данных, указанными в момент их использования, а не в момент объявления. Это обеспечивает безопасность типов на этапе компиляции и устраняет необходимость в приведении типов и упаковке/распаковке (boxing/unboxing) для значимых типов.

Проще говоря, дженерики позволяют писать код, который является типобезопасным и повторно используемым для различных типов данных.

Основные проблемы, которые решают дженерики

До появления дженериков в .NET 2.0 разработчики часто использовали необобщённые коллекции (например, ArrayList, Hashtable) из пространства имён System.Collections или тип object для создания универсальных структур данных. Это приводило к нескольким серьёзным проблемам:

1. Отсутствие безопасности типов (Type Safety)

Коллекции на основе object могут хранить элементы любого типа, что приводит к ошибкам во время выполнения (runtime errors).

// Проблема: ArrayList хранит object, возможна ошибка приведения
ArrayList list = new ArrayList();
list.Add(42);       // int
list.Add("текст");  // string - это допустимо!

int sum = 0;
foreach (int item in list) // Исключение InvalidCastException при втором элементе!
{
    sum += item;
}

2. Необходимость приведения типов (Casting)

При извлечении элементов из необобщённой коллекции требуется явное приведение типа, что усложняет код и скрывает ошибки.

ArrayList coordinates = new ArrayList();
coordinates.Add(new Vector3(1, 2, 3));

// Требуется явное и небезопасное приведение
Vector3 vec = (Vector3)coordinates[0];
// Что, если элемент не Vector3? - Runtime Exception!

3. Упаковка и распаковка (Boxing/Unboxing) для значимых типов

При добавлении значимого типа (например, int, struct) в коллекцию object происходит упаковка — выделение памяти в куче (heap) и копирование значения. При извлечении — распаковка с приведением. Это создает значительные накладные расходы на производительность.

ArrayList scores = new ArrayList();
scores.Add(95); // Boxing происходит здесь: int -> object

int retrievedScore = (int)scores[0]; // Unboxing происходит здесь: object -> int

Как дженерики решают эти проблемы?

Вместо ArrayList используется обобщённая коллекция List<T>, где T — параметр типа, задаваемый при создании экземпляра.

// Решение с дженериками: List<T> обеспечивает безопасность типов
List<int> intList = new List<int>();
intList.Add(42);
// intList.Add("текст"); // Ошибка компиляции! Компилятор не позволит добавить string

int sum = 0;
foreach (int item in intList) // Приведение не требуется, тип известен
{
    sum += item; // Безопасно и эффективно
}

// Аналогично для пользовательских типов
List<Vector3> vectors = new List<Vector3>();
vectors.Add(new Vector3(1, 2, 3));
Vector3 firstVector = vectors[0]; // Приведение не требуется!

Ключевые преимущества дженериков:

  • Безопасность типов: Компилятор проверяет соответствие типов на этапе компиляции, предотвращая InvalidCastException.
  • Повторное использование кода: Один обобщённый класс (например, List<T>) может использоваться для int, string, Player или любого другого типа.
  • Производительность: Устраняются затратные операции упаковки и распаковки для значимых типов. Обобщённый код работает напрямую с указанным типом.
  • Читаемость и удобство поддержки: Код становится чище, исчезают явные приведения типов, а намерения разработчика очевидны из объявления типа (List<Enemy>).

Пример создания простого обобщённого класса в контексте Unity

Представим, что нам нужен пул объектов для повторного использования игровых сущностей.

// Обобщённый класс пула объектов
public class GenericObjectPool<T> where T : Component // Ограничение: T должен быть Component (MonoBehaviour)
{
    private Queue<T> pool = new Queue<T>();
    private T prefab;

    public GenericObjectPool(T prefab, int initialSize)
    {
        this.prefab = prefab;
        for (int i = 0; i < initialSize; i++)
        {
            T obj = GameObject.Instantiate(prefab);
            obj.gameObject.SetActive(false);
            pool.Enqueue(obj);
        }
    }

    public T GetObject()
    {
        if (pool.Count > 0)
        {
            T obj = pool.Dequeue();
            obj.gameObject.SetActive(true);
            return obj; // Возвращаем конкретный тип T (Bullet, Enemy и т.д.)
        }
        else
        {
            return GameObject.Instantiate(prefab);
        }
    }

    public void ReturnObject(T obj)
    {
        obj.gameObject.SetActive(false);
        pool.Enqueue(obj);
    }
}

// Использование с разными типами
public class GameManager : MonoBehaviour
{
    public Bullet bulletPrefab;
    public Enemy enemyPrefab;

    private GenericObjectPool<Bullet> bulletPool; // Пул для пуль
    private GenericObjectPool<Enemy> enemyPool;   // Пул для врагов

    void Start()
    {
        // Создаём типобезопасные пулы. GetObject() возвращает Bullet или Enemy соответственно.
        bulletPool = new GenericObjectPool<Bullet>(bulletPrefab, 10);
        enemyPool = new GenericObjectPool<Enemy>(enemyPrefab, 5);

        Bullet newBullet = bulletPool.GetObject(); // Тип - Bullet, приведение не нужно
        Enemy newEnemy = enemyPool.GetObject();    // Тип - Enemy, приведение не нужно
    }
}

Итог: Дженерики — фундаментальная часть C#, которая делает код безопасным, производительным и гибким. В Unity они повсеместно используются в коллекциях (List<T>, Dictionary<TKey, TValue>), компонентах событий (UnityEvent<T>), и при правильном применении в пользовательских системах (как в примере с пулом) значительно повышают качество и сопровождаемость кода.

Что такое дженерики (Generics) в C#? Какие проблемы они решают? | PrepBro