Перейти к основному содержимому

Обобщения и обобщённое программирование

Разработчику Архитектору Инженеру

Связано

Маршрут по статье

  1. Определение и словарь терминов
  2. Зачем нужны обобщения — три практические причины
  3. Базовые понятия — параметры, ограничения, variance
  4. Классы и методы — синтаксис и различия
  5. Полиморфизм и STL-стиль
  6. Примеры — коллекции, алгоритмы, репозитории, async
  7. Реализация в языках — erasure, reification, мономорфизация
  8. Язык за языком — Java, C#, TypeScript, Go, Python, C++, Rust, Swift
  9. Углублённый разбор по каждому языку
  10. Паттерны, API, тесты · FAQ

Обобщения и обобщённое программирование

Определение

Дженерики (обобщения, обобщённое программирование, generic programming) — способ описать класс, интерфейс, функцию или метод один раз, а затем применять его к разным типам данных. Компилятор или статический анализатор проверяет совместимость типов до запуска программы.

Представьте коробку, в которую можно положить только то, что вы заранее обозначили на этикетке. Коробка для чисел не примет строку — ошибка всплывёт при сборке проекта, до запуска у пользователя.

КЛАСС Коробка<T>
поле значение: T
метод Получить(): T
вернуть значение
КОНЕЦ
КОНЕЦ

коробка_чисел := новый Коробка<Число>(42)
коробка_строк := новый Коробка<Строка>("привет")

Буква T здесь — параметр типа (заглушка). При создании Коробка<Число> заглушка заменяется на Число. Этот шаг называют инстанцированием — подстановкой конкретного типа вместо параметра.

Обобщения появились как ответ на практическую боль: библиотеки коллекций и алгоритмов писали заново под каждый тип или опускались до универсального Object / void*, теряя проверку на этапе компиляции. В Java 5 (2004) синтаксис <T> сделал List<String> стандартом. В C++ идея шире — шаблоны с конца 1980-х легли в основу STL. В Go дженерики пришли только в 1.18 (2022).

Два разных смысла слова "обобщить"

В ООП "обобщить" иногда значит вынести общие поля в базовый класс — иерархия AnimalDog. В этой статье речь о параметризации типа — запись List<T>, где T подставляется при использовании. Связь с ООП идёт через полиморфизм, но это отдельный его вид — параметрический.

Словарь терминов

ТерминОбъяснение
Параметр типаИмя-заглушка в объявлении (T, K, V, E). Буквы условны — T от type, K от key, V от value, E от element
Аргумент типаКонкретный тип при использовании. В List<int> аргумент — int
ИнстанцированиеМомент, когда T становится int, String или вашим классом Order
Безопасность типов (type safety)Гарантия, что операции выполняются только над совместимыми типами. Подробнее — типобезопасность
Приведение типа (casting)Явное преобразование ((String) obj). Обобщения уменьшают необходимость в нём — см. преобразование типов в C#
Сырой тип (raw type)Объявление без аргумента типа — List вместо List<String>. Остаток кода до Java 5
Ограничение (constraint, bound)Условие на параметр — T должен реализовать интерфейс Comparable
Верхняя граница (upper bound)T extends AnimalT может быть Animal или любым наследником
Нижняя граница (lower bound)? super Integer — подходит Integer, Number, Object
Вывод типов (type inference)Компилятор сам определяет T из контекста — var list = new ArrayList<>()
Wildcard? в Java — "неизвестный" аргумент типа в сигнатуре, часто с extends / super
Стирание типов (type erasure)После компиляции информация о T исчезает из байт-кода (Java на JVM)
Реификация (reification)Тип-параметр сохраняется в runtime (C# в CLR)
МономорфизацияКомпилятор генерирует отдельную версию кода для каждого аргумента типа (Rust, Go, C++)
Bridge methodСинтетический метод в байт-коде Java для совместимости erasure с переопределением
Reified typeТип, доступный в runtime (Kotlin reified T в inline-функции)
VarianceПравила подстановки List<Dog> там, где ожидают List<Animal>
ИнвариантностьПодстановка запрещена — List<Dog> нельзя присвоить List<Animal>
КовариантностьМожно читать как более общий тип (out / ? extends)
КонтравариантностьМожно писать в более общий приёмник (in / ? super)
Trait / концептНабор требований к операциям над T (Rust, C++20 concepts, Go constraints)
TypeVarВ Python — переменная для аннотаций T = TypeVar("T")
Utility typeВ TypeScript — преобразование типов Partial<T>, Pick<T, K>

Как называют в разных языках и дисциплинах

  • Дженерики / generics — Java, C#, Kotlin, Go, TypeScript, Swift
  • Обобщения — русскоязычные учебники и документация .NET
  • Шаблоны (templates) — C++; механизм богаче по выразительности, сложнее в отладке
  • Параметрический полиморфизм — термин из теории типов
  • Обобщённое программирование — широкий термин про алгоритмы и структуры, параметризованные типами (Википедия)

Зачем нужны обобщения

Безопасность типов

Статически типизированные языки проверяют совместимость типов при компиляции. Обобщения переносят эту проверку на контейнеры и алгоритмы.

Без параметра типа контейнер принимает любой объект. Ошибка "положили строку в список чисел" проявится только при извлечении — в Java это ClassCastException, в C# — InvalidCastException.

// Сырой контейнер — тип не указан
список := новый Список()
список.добавить(42)
список.добавить("ошибка") // компилятор пропускает
число := (Число) список.получить(1) // падение при запуске

// Обобщённый контейнер
список := новый Список<Число>()
список.добавить(42)
список.добавить("ошибка") // ошибка компиляции

Ошибка смещается ближе к автору — в IDE и при сборке, а не в логах продакшена. Для команды это означает дешевле ревью и меньше регрессий при рефакторинге.

Один алгоритм вместо копий

Принцип DRY ("не повторяйся") здесь работает буквально. Сортировка, поиск, кэш, очередь, репозиторий данных описываются один раз и работают с

  • целыми числами;
  • строками;
  • датами;
  • вашими моделями User, Order, Product.

Без обобщений остаются два неудобных пути

  • копировать функцию под каждый тип (sortInt, sortString, sortOrder);
  • опуститься до универсального object / void* и потерять проверку типов.

Пример дублирования, которое обобщения убирают:

функция НайтиПервыйЧисло(список: Список<Число>): Число?
// ... 15 строк логики ...
КОНЕЦ

функция НайтиПервуюСтроку(список: Список<Строка>): Строка?
// ... те же 15 строк, другой тип ...
КОНЕЦ

// С обобщением — одна реализация
функция НайтиПервый<T>(список: Список<T>, предикат: (T) -> Логический): T?
// ... 15 строк ...
КОНЕЦ

Меньше приведений типов

Из List<String> элемент приходит уже как String. Ручные (String), as string, @SuppressWarnings("unchecked") остаются редким исключением.

Каждое приведение — точка, где программист обещает компилятору знать больше, чем тот может проверить. Обобщения переносят это обещание в объявление типа: List<String> уже говорит "здесь только строки".

Документирование намерений

Сигнатура Map<UserId, Order> читается как спецификация

  • ключи — идентификаторы пользователей;
  • значения — заказы;
  • попытка положить Product в значение — ошибка компиляции.

Даже без чтения тела функции видно контракт API. Это особенно ценно в микросервисах и публичных библиотеках.


Базовые понятия подробнее

Параметр и аргумент типа

Параметр объявляется в угловых скобках при описании типа или функции. Аргумент подставляется при вызове или создании экземпляра.

// объявление — параметр T
функция Первый<T>(список: Список<T>): T

// использование — аргумент Заказ
результат := Первый<Заказ>(список_заказов)

// вывод типа — аргумент можно опустить
результат := Первый(список_заказов) // T выводится как Заказ

В Java и C# допустим diamond operatornew ArrayList<>() без повторения типа справа. Компилятор выводит аргумент из левой части присваивания:

список: Список<Строка> = новый МассивСписок<>()
// ^^^^^^^^^^^^^^
// diamond — тип Строка взят слева

Важно для новичка: угловые скобки <> в современных языках означают именно типы, а не сравнение "меньше". В статье про знаки в коде это разобрано отдельно.

Ограничения (constraints)

Параметр T без ограничений означает "любой тип". Внутри функции вы не сможете вызвать методы, которых у произвольного типа нет. Ограничение сужает множество допустимых типов и открывает доступ к методам интерфейса.

функция Максимум<T где T : Сравнимый>(a: T, b: T): T
если a > b
вернуть a
иначе
вернуть b
конец
КОНЕЦ

функция Вывести<T где T : Выводимый>(значение: T)
значение.Вывести()
КОНЕЦ

функция Создать<T где T : Сущность, T имеет конструктор()>(ид: Ид): T
вернуть новый T()
КОНЕЦ

Синтаксис ограничений по языкам

ЯзыкПример
C#where T : IComparable<T> · where T : class, new()
Java<T extends Comparable<? super T>>
Rustfn f<T: Ord + Clone>(x: T)
Gofunc Min[T cmp.Ordered](a, b T) T
TypeScript<T extends { id: string }>
Kotlin<T : Comparable<T>> · where T : Entity

Несколько ограничений можно комбинировать — T должен одновременно быть сущностью и поддерживать сравнение.

Ковариантность и контравариантность

Если Dog наследует Animal, логично ожидать, что List<Dog> подойдёт туда, где ждут List<Animal>. На практике обобщённые коллекции в Java инвариантны — такая подстановка запрещена. Иначе в список собак можно было бы незаметно добавить кота через ссылку на List<Animal>.

собаки: Список<Dog> = ...
животные: Список<Animal> = собаки // ОШИБКА в Java

// Если бы компилятор разрешил:
животные.добавить(new Cat()) // в списке собак появился бы кот

Три режима variance

РежимСмыслКлючевые слова
ИнвариантностьList<Dog> и List<Animal> несовместимыобычный List<T> в Java
КовариантностьМожно читать как базовый тип, писать нельзяKotlin out, Java ? extends
КонтравариантностьМожно принимать базовый тип, отдавать узкийKotlin in, Java ? super

PECS (Producer Extends, Consumer Super) — мнемоника для Java

  • Producer (источник элементов) — ? extends T
  • Consumer (приёмник элементов) — ? super T
// читаем из списка — producer
функция Суммировать(числа: Список<? extends Число>): Число

// пишем в список — consumer
функция ЗаполнитьНулями(список: Список<? super Целое>)

Подробности по Kotlin — generics и variance.

Несколько параметров типа

КЛАСС Словарь<K, V>
метод Получить(ключ: K): V
метод Записать(ключ: K, значение: V)
КОНЕЦ

функция СоздатьПару<A, B>(первый: A, второй: B): Пара<A, B>
вернуть новый Пара(первый, второй)
КОНЕЦ

функция Преобразовать<T, R>(список: Список<T>, f: (T) -> R): Список<R>
результат := новый Список<R>()
для каждого x в список
результат.добавить(f(x))
конец
вернуть результат
КОНЕЦ

K и V в словаре часто разные. A и B в паре связывают тип результата с типами аргументов. T и R в Преобразовать связывают тип входного элемента с типом выходного — классический map из функционального стиля.

Вложенные обобщения

матрица: Список<Список<Число>> // таблица чисел
кэш: Словарь<Строка, Список<Заказ>> // ключ → список заказов
опционально: Опционально<Список<T>> // может не быть списка

Читается изнутри наружу: Список<Число> — список чисел; внешний Список<...> — список таких списков.

В TypeScript вложенные generic особенно выразительны — связанные типы и infer.


Обобщённый класс и обобщённый метод

Обобщать можно тип целиком или отдельный метод внутри обычного класса.

Обобщённый класс

КЛАСС Стек<T>
поле элементы: Список<T>

метод Положить(значение: T)
элементы.добавить(значение)
КОНЕЦ

метод Снять(): T
вернуть элементы.удалитьПоследний()
КОНЕЦ
КОНЕЦ

стек_чисел := новый Стек<Число>()
стек_строк := новый Стек<Строка>()

Параметр T один на весь класс — все методы работают с одним и тем же типом элемента.

Обобщённый метод

КЛАСС Утилиты
// статический обобщённый метод — свой T, независимый от класса
статический метод ПоменятьМестами<T>(a: Указатель<T>, b: Указатель<T>)
временный := a.*
a.* := b.*
b.* := временный
КОНЕЦ
КОНЕЦ

x := 1
y := 2
Утилиты.ПоменятьМестами<Число>(ссылка(x), ссылка(y))

В Java Collections.sort(list) — обобщённый статический метод в классе, который сам не параметризован.

Обобщённый классОбобщённый метод
Где объявлен TВ заголовке классаВ заголовке метода
Область TВсе поля и методы классаТолько этот метод
ПримерArrayList<E>Collections.sort<T>(list)
Типичная задачаКонтейнер с одним типом элементаУтилита, работающая с любым T

Обобщённый интерфейс

ИНТЕРФЕЙС Сравнимый<T>
метод СравнитьС(другой: T): Целое
КОНЕЦ

ИНТЕРФЕЙС Репозиторий<T где T : Сущность>
метод Найти(ид: Ид): T?
метод Сохранить(сущность: T)
КОНЕЦ

Сравнимый<T> в Java означает "этот объект сравнивается с другими того же типа T". Репозиторий<T> — контракт доступа к сущностям одного вида.

Наследование и обобщения

КЛАСС Животное
поле кличка: Строка
КОНЕЦ

КЛАСС Питомник<T где T : Животное>
поле жители: Список<T>

метод Добавить(животное: T)
жители.добавить(животное)
КОНЕЦ
КОНЕЦ

КЛАСС ПитомникСобак РАСШИРЯЕТ Питомник<Dog>
// T зафиксирован как Dog
КОНЕЦ

Подкласс может зафиксировать параметр типа (Питомник<Dog>) или оставить его свободным.


Полиморфизм и обобщения

Полиморфизм — способность одного интерфейса работать с разными реализациями. В информатике выделяют три часто встречающихся вида.

ВидИдеяПример
ПараметрическийКод один, тип передаётся параметромList<T>, fn identity<T>(x: T)
ПодтипыОбщий базовый тип, разные классы-наследникиФигура f = new Круг()
Ad hocОдно имя, разные наборы параметров (перегрузка)сложить(int,int) и сложить(String,String)

В разговорах про ООП слово "полиморфизм" обычно относится к подтипам и переопределению методов (статья 4.08.05). Дженерики относятся к параметрическому полиморфизму — конкретный тип передаётся в алгоритм параметром, без иерархии наследования.

Три вида дополняют друг друга в одном приложении

  • List<Animal> хранит элементы одного заданного типа (параметрический);
  • метод draw() вызывается у каждого Animal, реализация зависит от Dog / Cat (подтипы);
  • перегрузка format(Date) и format(int) (ad hoc).

Обобщённое программирование в стиле STL

Обобщённое программирование как стиль (библиотека STL в C++, заголовок <algorithm>) шире синтаксиса List<T> в Java. Алгоритм описывают относительно возможностей типа, а не конкретного класса.

Строительные блоки

  • Итератор — способ обхода элементов без знания внутренней структуры контейнера
  • Компаратор — правило сравнения двух элементов
  • Trait / концепт — набор операций, которые тип обязан поддерживать
  • Алгоритмsort, find, accumulate, параметризованный итераторами
// Псевдокод в духе STL
функция Сортировать<Итератор где Итератор : Проходимый>(начало, конец: Итератор)
// сортировка на месте, не зная — vector это или list
КОНЕЦ

функция Накопить<Итератор, T>(начало, конец: Итератор, начальное: T, операция): T
аккумулятор := начальное
для каждого x от начало до конец
аккумулятор := операция(аккумулятор, x)
конец
вернуть аккумулятор
КОНЕЦ

Тот же принцип в других экосистемах

Связь с функциональной парадигмойmap, filter, reduce естественно выражаются через Преобразовать<T, R> и подобные сигнатуры.


Примеры на практике

Коллекции

Первое знакомство с дженериками — типобезопасные контейнеры.

числа := новый Список<Число>()
числа.добавить(1)
числа.добавить(2)

имена := новый Словарь<Строка, Пользователь>()
имена.записать("alice", пользователь)

уникальные_теги := новое Множество<Строка>()
уникальные_теги.добавить("java")

Типичные интерфейсы коллекций

ИнтерфейсРольПример аргумента
List<T>Упорядоченный список с индексамиList<Order>
Set<T>Множество без дубликатовSet<String>
Map<K,V>Пары ключ-значениеMap<UserId, User>
Queue<T>Очередь FIFOQueue<Task>
Deque<T>Двусторонняя очередьDeque<Event>

Теория структур

Универсальные алгоритмы

функция Найти<T>(список: Список<T>, предикат: (T) -> Логический): T?
для каждого элемента в список
если предикат(элемент)
вернуть элемент
конец
конец
вернуть пусто
КОНЕЦ

функция Сгруппировать<T, K>(список: Список<T>, ключ: (T) -> K): Словарь<K, Список<T>>
результат := новый Словарь<K, Список<T>>()
для каждого элемента в список
k := ключ(элемент)
если не результат.содержит(k)
результат.записать(k, новый Список<T>())
конец
результат.получить(k).добавить(элемент)
конец
вернуть результат
КОНЕЦ

результат := Найти(заказы, λ o → o.сумма > 1000)
по_статусу := Сгруппировать(заказы, λ o → o.статус)

T может быть заказом, пользователем или строкой. K в группировке — тип ключа (строка статуса, дата, идентификатор). Аналоги в стандартных библиотеках

  • Java — Stream.filter, Collectors.groupingBy
  • C# — LINQ Where, GroupBy
  • Python — filter, itertools.groupby (типы через аннотации)
  • Go — slices.ContainsFunc, generics-функции в stdlib

См. алгоритмы — раздел 4.01.

Репозитории и сервисы

ИНТЕРФЕЙС Репозиторий<T где T : Сущность>
метод НайтиПоИд(ид: Ид): T?
метод Сохранить(сущность: T)
метод Удалить(сущность: T)
метод Все(): Список<T>
КОНЕЦ

КЛАСС РепозиторийЗаказов РЕАЛИЗУЕТ Репозиторий<Заказ>
// SQL или ORM под капотом
КОНЕЦ

КЛАСС СервисЗаказов
поле репозиторий: Репозиторий<Заказ>

метод Оформить(заказ: Заказ)
репозиторий.Сохранить(заказ)
КОНЕЦ
КОНЕЦ

Паттерн IRepository<T> распространён в enterprise-коде на C# и Java. Ограничение T : Сущность гарантирует наличие идентификатора и метаданных persistence. Связанные темы — ORM, зависимости.

Optional, Result и обёртки

Опционально<T> // значение может отсутствовать
Результат<T, E> // успех T или ошибка E
Обещание<T> // асинхронный результат типа T

Optional<T>, Result<T, E>, Promise<T> описывают форму ответа, не привязываясь к конкретному содержимому.

функция НайтиПользователя(ид: Ид): Опционально<Пользователь>
// ...
КОНЕЦ

асинхронная функция ЗагрузитьЗаказы(): Обещание<Список<Заказ>>
// ...
КОНЕЦ

В TypeScript Promise<User> связывает async-функцию с типом результата (async в TS). В Rust — Option<T> и Result<T, E> встроены в язык (справочник Rust).

Фабрики и построители

ИНТЕРФЕЙС Фабрика<T>
метод Создать(): T
КОНЕЦ

КЛАСС ФабрикаЗаказов РЕАЛИЗУЕТ Фабрика<Заказ>
метод Создать(): Заказ
вернуть новый Заказ()
КОНЕЦ
КОНЕЦ

КЛАСС Построитель<T>
метод Сбросить(): Построитель<T>
метод Установить(поле, значение): Построитель<T>
метод Собрать(): T
КОНЕЦ

Generic-фабрики входят в паттерны GoF (Factory Method, Builder) и в DI-контейнеры.

Кэши и пулы

КЛАСС Кэш<K, V>
поле хранилище: Словарь<K, V>
поле максимум: Целое

метод ПолучитьИлиВычислить(ключ: K, вычислить: () -> V): V
если хранилище.содержит(ключ)
вернуть хранилище.получить(ключ)
конец
значение := вычислить()
хранилище.записать(ключ, значение)
вернуть значение
КОНЕЦ
КОНЕЦ

K — тип ключа (строка URL, UUID), V — тип значения (объект, JSON-дерево, байты).

События и сообщения

ИНТЕРФЕЙС ОбработчикСобытия<T>
метод Обработать(событие: T)
КОНЕЦ

КЛАСС ШинаСобытий
поле подписчики: Словарь<Тип, Список<Обработчик>>

метод Подписать<T>(обработчик: ОбработчикСобытия<T>)
метод Опубликовать<T>(событие: T)
КОНЕЦ

Type-safe event bus в TypeScript разобран в паттернах TS.


Как языки реализуют обобщения

Все перечисленные языки решают одну задачу — параметризацию типов. Различаются компромиссы между совместимостью со старым кодом, размером бинарника, скоростью и доступом к типу в runtime.

ЯзыкМеханизмЧто важно знать
Javaстирание типовВ байт-коде List<String> превращается в List. Старые JAR без generics продолжают работать. Типы в Java
C#реификация в CLRList<int> существует в runtime. where T : IComparable, рефлексия по T. Обобщения в C#
Kotlinerasure на JVM + reifiedVariance на объявлении (out/in). Kotlin generics
TypeScriptтолько compile-timeПосле компиляции в JS типы исчезают. Дженерики в TS
Goмономорфизация с 1.18Отдельные функции под каждый тип при сборке. Дженерики в Go
Pythontyping, проверка снаружиlist[int], Generic[T] — для IDE, mypy, pyright. Типы
C++шаблоны при компиляцииМаксимальная гибкость, длинные ошибки. Concepts с C++20. О C++
Rustgenerics + traitsМономорфизация, нулевая стоимость в runtime. Справочник §13
Swiftgenerics + протоколыAssociated types, where. Протоколы

Стирание типов

После компиляции информация о T исчезает из исполняемого кода. Проверки уже выполнены, в runtime остаётся совместимость со старым байт-кодом.

// Исходник
List<String> names = new ArrayList<>();
names.add("Alice");
String s = names.get(0);

// После erasure (логически)
List names = new ArrayList();
names.add("Alice"); // проверка вставлена компилятором
String s = (String) names.get(0); // скрытое приведение

Следствия для Java

  • нельзя new T()
  • нельзя instanceof List<String>
  • нельзя создать new T[]
  • примитивы только через обёртки — List<Integer>, не List<int>
  • bridge methods при переопределении generic-методов

Плюсы — бинарная совместимость с кодом 2003 года. Минусы — ограниченная рефлексия по T.

Реификация

Тип-параметр сохраняется в runtime. CLR знает, что перед вами List<string>, а не просто List.

List<String> ──компиляция──► List`1[[System.String]]

Возможности C#

  • typeof(List<int>) отличается от typeof(List<string>)
  • where T : new() — вызов new T()
  • default(T) для значений по умолчанию
  • рефлексия по открытым generic-типам

Платформа .NET спроектирована с учётом reification с самого начала.

Мономорфизация

Компилятор разворачивает отдельную специализацию под каждый использованный аргумент типа.

sort<int> ──компиляция──► машинный код сравнения int
sort<string> ──компиляция──► машинный код сравнения string

Плюсы

  • нулевая стоимость абстракции в runtime (идеал Rust);
  • не нужны boxing-приёмы для примитивов.

Минусы

  • дольше сборка;
  • больше размер бинарника при многих T;
  • длинные ошибки в C++ при ошибке в шаблоне.

Связанные темы


Подробнее по языкам

Java

Синтаксис с Java 5. Erasure на JVM — главное ограничение.

List<String> list = new ArrayList<>();
Map<Integer, User> byId = new HashMap<>();

Ключевые темы

  • wildcards ? extends / ? superтипы в Java
  • примитивы через обёртки Integer, Long
  • var и diamond <> с Java 10+
  • Stream API — Stream<T>, Collectors

Типичный вопрос на собеседовании — почему List<Object> не принимает List<String> (инвариантность).

C#

Reified generics в CLR — один из самых удобных для прикладного кода вариантов.

List<int> numbers = new();
Dictionary<string, Order> orders = new();

Возможности

  • ограничения where T : class, struct, new(), интерфейсы
  • nullable reference types вместе с T?
  • generic-делегаты Func<T, R>, Action<T>
  • LINQ — цепочки над IEnumerable<T>

Подробно — обобщения в C#, коллекции.

Kotlin

Совместимость с Java erasure, но синтаксис и variance удобнее.

val list: List<String> = listOf("a", "b")
interface Repository<out T> // ковариантный producer

Особенности

  • declaration-site variance out / in
  • reified T в inline fun — тип в runtime внутри inline-тела
  • star projection List<*> — аналог Java raw с безопасностью
  • reified generics

TypeScript

Generics существуют только при проверке типов. В JavaScript после компиляции типов нет.

function identity<T>(x: T): T { return x; }
type ApiResponse<T> = { data: T; error?: string };

Сильные стороны

  • constraints <T extends HasId>
  • связанные generic — тип поля зависит от ключа
  • utility types Partial<T>, Pick<T, K>, Record<K, V>
  • conditional types и infer

Дженерики в TypeScript · утилитарные типы

Go

До 1.18 — интерфейсы и interface{} / any. С 1.18 — type parameters.

func Min[T cmp.Ordered](a, b T) T { ... }
type Stack[T any] struct { items []T }

Философия — generics для библиотечных алгоритмов, доменная модель на интерфейсах. Дженерики в Go.

Python

Динамическая типизация в runtime. Аннотации — для людей и анализаторов.

from typing import TypeVar, Generic, List

T = TypeVar("T")

class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value

Инструменты

  • mypy, pyright, pyre
  • list[int] с Python 3.9+
  • Protocol — структурные ограничения без наследования

Типы в Python · typing · mypy

C++

Шаблоны — основа STL, мощнее и сложнее классических generics.

template<typename T>
T max(T a, T b) { return a < b ? b : a; }

template<typename It, typename Cmp>
void sort(It first, It last, Cmp cmp);

С C++20 — concepts ограничивают T декларативно. Ошибки компиляции всё ещё могут быть многостраничными — выносите сложные выражения в именованные alias.

О C++ · type traits

Rust

Generics + traits, мономорфизация, без GC.

fn largest<T: PartialOrd>(list: &[T]) -> &T { ... }
struct Vec<T> { ... }

Option<T>, Result<T, E>, итераторы Iterator<Item = T> — везде generics. Справочник §13.

Swift

Протокол-ориентированный стиль + generics.

func swap<T>(_ a: inout T, _ b: inout T) { ... }
struct Stack<Element> { ... }

Associated types в протоколах, where Element: Comparable. Протоколы и generics.


Производительность и размер кода

СтратегияRuntimeРазмер бинарникаВремя компиляции
ErasureНакладные расходы на boxing примитивовОдин класс ListБыстрее
ReificationНативные специализации value types в .NETОтдельные generic-инстансыСредне
МономорфизацияОптимальный код под каждый TРост при многих TДольше

Для "горячих" циклов с примитивами в Java иногда используют специализированные библиотеки (IntArrayList и т.п.) или массивы int[]. В Rust и C++ мономорфизация даёт тот же эффект автоматически.

Связано — сборка мусора и boxing.


Практические правила

  • Параметризуйте алгоритм, когда его логика одинакова для разных типов — сортировка, поиск, маппинг, очередь, кэш Кэш<K, V>.
  • Добавляйте ограничение, если внутри вызываются методы интерфейса — укажите T : Comparable, T : Entity или T : Display вместо голого T без требований.
  • Откладывайте абстракцию, пока в проекте один конкретный тип. Лишний Repository<T> усложняет чтение без выгоды.
  • Указывайте аргумент типа у коллекций — пишите List<Order> вместо сырого List в Java или legacy C#.
  • Проверяйте variance, прежде чем присваивать List<Dog> в List<Animal>. В Java списки инвариантны; для чтения в Kotlin — List<out Animal>.
  • В Python запускайте mypy или pyright в CI — аннотации сами по себе runtime не защищают.
  • В TypeScript не полагайтесь на типы в runtime — валидация входа через zod/io-ts при границе API.
  • В C++ выносите сложные шаблоны в именованные alias и type traits — проще читать ошибки компилятора.

Когда обобщение уместно

  • один и тот же алгоритм для нескольких типов;
  • библиотечный код с публичным API;
  • контейнеры и инфраструктурные типы (Result, Page<T>);
  • тестовые фабрики и builders.

Когда лучше конкретный тип

  • единственный тип в проекте и в обозримом будущем;
  • доменная логика, где абстракция скрывает бизнес-правила;
  • производительность критична, а специализация проще ручного кода (редко).
Первое упражнение

Возьмите задачу "список заказов" или "найти элемент по условию". Перепишите её с параметром типа вместо object или дублирования под int и string. Затем откройте стандартную библиотеку своего языка — java.util.Collections, System.Collections.Generic, std::vector, пакет slices в Go — и найдите объявления generic-типов.

Цепочка упражнений

  1. Стек<T> с положить / снять на псевдокоде
  2. Найти<T> с предикатом
  3. Словарь<K,V> с методом получитьИлиВычислить
  4. Репозиторий<T extends Entity> с заглушкой in-memory
  5. Переписать (4) на своём языке по ссылке из таблицы материалов

История появления обобщений

Идея параметризации типов зрела параллельно с ростом библиотек коллекций и алгоритмов.

ПериодСобытие
1970-еML — параметрический полиморфизм в функциональных языках
1980-еAda generics, CLU parameterized clusters
1989–1991C++ templates, затем STL (Stepanov) — обобщённые алгоритмы и итераторы
До 2004Java 1.4 — Vector, Hashtable хранят Object, приведения вручную
2004Java 5 — синтаксис <T>, erasure ради совместимости байт-кода
2002C# 2.0 — generics с reification в CLR
2010-еKotlin, Swift — variance и протоколы поверх JVM/LLVM
2015+TypeScript — generics для статической проверки JS-кода
2022Go 1.18 — generics после долгого периода с интерфейсами и go generate

В Java выбор erasure объясняется требованием: старые .class без generics должны работать с новым кодом без перекомпиляции (история Java 5). В C# платформа проектировалась заново с reification. В Rust и Go упор на мономорфизацию и предсказуемую производительность.


Пошаговый разбор для новичка

Задача: хранить список заказов интернет-магазина и найти заказы дороже 1000.

Шаг 1. Без типизации

список := новый Список() // что угодно
список.добавить(заказ1)
список.добавить("случайная строка") // компилятор молчит

для i от 0 до список.длина() - 1
элемент := (Заказ) список.получить(i) // ClassCastException на строке
если элемент.сумма > 1000
// ...
конец
конец

Шаг 2. С обобщением

список: Список<Заказ> = новый МассивСписок<>()
список.добавить(заказ1)
список.добавить("случайная строка") // ошибка компиляции

для каждого заказ в список
если заказ.сумма > 1000
// заказ уже типа Заказ, без приведения
конец
конец

Шаг 3. Вынос алгоритма

функция Дороже<T где T имеет поле сумма: Число>(список: Список<T>, порог: Число): Список<T>
результат := новый Список<T>()
для каждого элемент в список
если элемент.сумма > порог
результат.добавить(элемент)
конец
конец
вернуть результат
КОНЕЦ

дорогие := Дороже(список_заказов, 1000)

Тот же Дороже сработает для Список<Продукт> с полем цена, если constraint описывает интерфейс ИмеетЦену.

Связанные материалы — перебор коллекций, Lab — коллекции.


Решения упражнений (псевдокод)

Упражнение 1 — Стек<T>

КЛАСС Стек<T>
поле элементы: Список<T> = новый МассивСписок<>()

метод Положить(значение: T)
элементы.добавить(значение)
КОНЕЦ

метод Снять(): T
если элементы.пусто()
бросить ИсключениеПустогоСтека
конец
вернуть элементы.удалитьПоследний()
КОНЕЦ

метод Верх(): T
вернуть элементы.получить(элементы.длина() - 1)
КОНЕЦ

метод Пусто(): Логический
вернуть элементы.пусто()
КОНЕЦ
КОНЕЦ

Упражнение 2 — Найти<T>

функция Найти<T>(список: Список<T>, предикат: (T) -> Логический): Опционально<T>
для каждого x в список
если предикат(x)
вернуть Опционально.из(x)
конец
конец
вернуть Опционально.пусто()
КОНЕЦ

Упражнение 3 — получитьИлиВычислить

КЛАСС Словарь<K, V>
поле данные: ХешТаблица<K, V>

метод ПолучитьИлиВычислить(ключ: K, вычислить: (K) -> V): V
если данные.содержит(ключ)
вернуть данные.получить(ключ)
конец
значение := вычислить(ключ)
данные.записать(ключ, значение)
вернуть значение
КОНЕЦ
КОНЕЦ

Упражнение 4 — in-memory репозиторий

ИНТЕРФЕЙС Сущность
свойство Ид: Ид
КОНЕЦ

КЛАСС ПамятьРепозиторий<T где T : Сущность> РЕАЛИЗУЕТ Репозиторий<T>
поле хранилище: Словарь<Ид, T> = новый ХешТаблица<>()

метод НайтиПоИд(ид: Ид): T?
вернуть хранилище.получить(ид)
КОНЕЦ

метод Сохранить(сущность: T)
хранилище.записать(сущность.Ид, сущность)
КОНЕЦ
КОНЕЦ

Паттерны проектирования и обобщения

Обобщения часто встречаются в паттернах GoF.

Strategy

ИНТЕРФЕЙС СтратегияСкидки
метод Применить(заказ: Заказ): Деньги
КОНЕЦ

КЛАСС КонтекстРасчёта<T где T : Заказ>
поле стратегия: СтратегияСкидки

метод Итог(заказ: T): Деньги
вернуть стратегия.Применить(заказ)
КОНЕЦ
КОНЕЦ

Adapter

КЛАСС АдаптерРепозитория<T, DTO>
поле внутренний: Репозиторий<T>
поле в_dto: (T) -> DTO
поле из_dto: (DTO) -> T

метод Найти(ид: Ид): DTO?
сущность := внутренний.НайтиПоИд(ид)
если сущность = пусто
вернуть пусто
конец
вернуть в_dto(сущность)
КОНЕЦ
КОНЕЦ

Factory Method

ИНТЕРФЕЙС ФабрикаДокументов<T>
метод Создать(): T
КОНЕЦ

Параметр T фиксирует тип продукта фабрики без дублирования иерархий под каждый документ.


REST API и обобщённые ответы

Типичные обёртки HTTP API параметризуют полезную нагрузку.

СТРУКТУРА Ответ<T>
поле данные: T
поле ошибка: Опционально<Строка>
поле мета: Опционально<Мета>
КОНЕЦ

СТРУКТУРА Страница<T>
поле элементы: Список<T>
поле всего: Целое
поле страница: Целое
поле размер: Целое
КОНЕЦ

Клиент TypeScript знает, что GET /users возвращает Ответ<Список<Пользователь>>, а GET /users/:idОтвет<Пользователь>. См. как работают сайты, интеграции.


Сериализация и JSON

При преобразовании объектов в JSON тип T может теряться, если библиотека не знает о generics.

Проблемы

  • Java erasure — List<User> десериализуется как List с LinkedHashMap внутри
  • TypeScript — типы не существуют в runtime, нужна валидация схемы
  • C# — JsonSerializer.Deserialize<List<User>> работает благодаря reification

Обходные пути в Java

  • передавать TypeReference<List<User>> (Jackson)
  • TypeToken<List<User>> (Gson)
  • явный Class<T> в фабрике
функция Десериализовать<T>(json: Строка, тип: Класс<T>): T
// библиотека использует тип в runtime через Class
КОНЕЦ

См. конфигурации и данные.


Зависимости и внедрение (DI)

DI-контейнеры регистрируют открытые generic-типы.

// Псевдокод регистрации
контейнер.зарегистрироватьОткрытый(typeof(Репозиторий<>), typeof(РепозиторийEf<>))
контейнер.зарегистрировать(typeof(СервисЗаказов))

При запросе Репозиторий<Заказ> контейнер подставляет РепозиторийEf<Заказ>. В C# это стандарт ASP.NET Core; в Java — Spring Repository<T>. Подробнее — зависимости.


Тестирование обобщённого кода

Принципы

  • тестируйте конкретный инстанс generic — Стек<Число>, Стек<Строка>;
  • для алгоритмов используйте property-based подход — один тест с разными T;
  • моки интерфейсов Репозиторий<T> создаются с тем же T, что и продакшен.
тест "Стек возвращает LIFO"
стек := новый Стек<Целое>()
стек.положить(1)
стек.положить(2)
утвердить стек.снять() == 2
утвердить стек.снять() == 1
конец

тест "Найти работает для строк и чисел"
для каждого типа в [Целое, Строка]
// параметризованный тест фреймворка
конец
конец

См. тестирование (общие принципы качества).


Рефлексия и обобщения

ЯзыкДоступ к T в runtime
JavaОграничен erasure; ParameterizedType, TypeVariable
C#Полный — typeof(T), MakeGenericType
Kotlinreified только в inline; иначе как Java
TypeScriptНет в JS; typeof для значений
Pythontyping.get_args, get_origin
RustTypeId для конкретных типов, не для параметра
Goreflect.Type, generics не расширяют reflect полностью

Пример Java — узнать аргументы у поля

Field field = ...;
Type type = field.getGenericType();
if (type instanceof ParameterizedType pt) {
Type[] args = pt.getActualTypeArguments();
}

В прикладном коде рефлексия по generics — крайняя мера; предпочтительны явные фабрики и API библиотек сериализации.


Антипаттерны

Generic hell

Слишком много параметров T, U, V, K, R в одной сигнатуре. Читатель теряет связь между типами.

Признак — сигнатура не помещается в одну строку без прокрутки.

Лечение — именованные типы-обёртки, промежуточные record / struct.

Object everywhere

функция Обработать(данные: Object): Object // потеряны generics

Возврат к эпохе до Java 5. Замените на <T> T обработать(T данные) или конкретный тип.

Преждевременный Repository<T>

Один тип сущности в микросервисе — OrderRepository без параметра читается проще, чем Repository<Order> без второго использования T.

Подавление предупреждений

@SuppressWarnings("unchecked") на каждом методе — сигнал, что типы обошли систему. Локализуйте участок и документируйте, почему безопасно.


Справочник ограничений C#

ОграничениеСмысл
where T : structТолько value type
where T : classТолько ссылочный тип
where T : class?Ссылочный, допускает null
where T : notnullНе nullable value
where T : new()Публичный конструктор без параметров
where T : BaseClassНаследник BaseClass
where T : IInterfaceРеализует интерфейс
where T : UT наследует другой параметр U

Комбинации — where T : class, IDisposable, new().

Полный разбор — обобщения в C#.


Справочник wildcards Java

ЗаписьЧтениеЗапись в коллекцию
List<?>элементы неизвестного типатолько null
List<? extends Number>числа и подтипызапрещена (кроме null)
List<? super Integer>Integer и любые супертипыможно add Integer
List<T>инвариантноadd T

Мнемоника PECSраздел variance выше.


Обобщения и функциональный стиль

В функциональной парадигме generic-функции естественно сочетаются с чистыми преобразованиями.

функция Карта<T, R>(список: Список<T>, f: (T) -> R): Список<R>
функция Фильтр<T>(список: Список<T>, p: (T) -> Логический): Список<T>
функция Свернуть<T, R>(список: Список<T>, начало: R, f: (R, T) -> R): R
ОперацияТипыРезультат
КартаTRСписок<R>
ФильтрT → ЛогическийСписок<T>
СвернутьR + TRR

В Haskell и ML параметрический полиморфизм — основа языка. В JavaScript без статических типов те же идеи в array.map / filter / reduce; TypeScript добавляет <T> к ним (JS-курс).


Обобщения в стандартных библиотеках

Где искать живые примеры в документации

ПлатформаТипы для изучения
Javajava.util.List, Optional, Stream, CompletableFuture
.NETList<T>, Dictionary<K,V>, Task<T>, IEnumerable<T>
TypeScriptArray<T>, Promise<T>, Record<K,V>
RustVec<T>, Option<T>, Result<T,E>, Iterator
Goslices, maps, sync.Map (не generic), generic min/max
C++std::vector<T>, std::optional<T>, std::variant<T...>

Откройте исходник или документацию одного типа и проследите, как объявлен параметр T и какие ограничения наложены.


Связь с метапрограммированием

Метапрограммирование и обобщения пересекаются, но не совпадают.

  • Generics — параметризация на уровне типов в исходном коде
  • Шаблоны C++ — могут вычислять типы на этапе компиляции (SFINAE, constexpr)
  • Макросы Rust — генерируют код, иногда дублируя роль generics
  • Source Generators C# — генерируют типобезопасный код из T в compile-time

Обобщения — самый безопасный и читаемый уровень; метапрограммирование подключают, когда generics не хватает (генерация бойлерплейта, ORM-маппинг).


Дополнительные вопросы

Вопрос. Можно ли использовать примитивы в Java generics?

Ответ. Только через обёртки — List<Integer>, не List<int>. Boxing добавляет накладные расходы. Для hot-path — массивы int[] или специализированные коллекции — типы в Java.

Вопрос. Что такое F-bounded polymorphism?

Ответ. Ограничение вида T extends Comparable<T> — тип сравнивается сам с собой. Встречается в Java для корректного compareTo.

Вопрос. Есть ли generics в JavaScript?

Ответ. В рантайме JS их нет. TypeScript добавляет синтаксис при компиляции — TypeScript, JS-курс.

Вопрос. Как generics связаны с null-safety?

Ответ. В C# 8+ T? для unconstrained T означает nullable reference type. В Kotlin T по умолчанию non-null, если не указан T?. В Java все ссылочные типы в generics допускают null, если не используются аннотации @NonNull.

Вопрос. Влияют ли generics на производительность?

Ответ. После компиляции — обычно нет (erasure, мономорфизация). Исключение — boxing примитивов в Java и размер бинарника при многих T в Rust/C++. См. производительность.

Вопрос. Что читать после этой статьи?

Ответ. Статью по своему языку из таблицы, затем коллекции и полиморфизм.


Частые ошибки

ОшибкаПоследствиеРешение
Сырой List без <T>Потеря проверки типовВсегда указывать аргумент типа
object / any вместо TТе же runtime-ошибки, что у сырого контейнераПараметр типа и constraint
Слишком широкий TНельзя вызвать нужный метод внутриwhere T : IComparable и аналоги
Игнорирование erasure в Javainstanceof List<String> не компилируетсяClass<T>, фабрики, TypeToken (Gson)
Глубокая вложенность шаблонов C++Страницы текста в ошибке компилятораУпростить выражение, concepts, явные типы
Аннотации Python без линтераОшибки типов только в голове автораmypy/pyright в CI
List<Animal> animals = dogsЛогическая дыра в типахPECS, ковариантные типы только для чтения
Generic array new T[n] в JavaОшибка компиляцииArrayList, (T[]) new Object[n] с осторожностью

Частые вопросы

Вопрос. Чем дженерики отличаются от наследования?

Ответ. Наследование связывает классы в иерархию — Dog extends Animal. Дженерики параметризуют тип внутри одного объявления — List<T>. Подтипы — полиморфизм через переопределение методов; обобщения — через подстановку типа. См. полиморфизм и наследование.

Вопрос. Почему нельзя создать массив generic-типа в Java?

Ответ. Из-за erasure массив в runtime помнит только Object[], а компилятор думал бы, что это String[] — нарушение типобезопасности. Используйте ArrayList<T> или массивы с осторожным приведением.

Вопрос. Нужны ли дженерики в динамическом Python?

Ответ. Интерпретатор их игнорирует, но аннотации и Generic[T] помогают IDE, ревью и mypy ловить ошибки до деплоя. В больших кодовых базах это стандарт практики — см. стиль Python.

Вопрос. Generics в Go — замена интерфейсам?

Ответ. Нет. Интерфейсы описывают поведение (io.Reader). Generics — алгоритмы над разными типами данных (slices.Contains). В Go оба механизма сосуществуют — дженерики в Go.

Вопрос. Что такое reified type в Kotlin?

Ответ. В обычной JVM-функции T стёрт. В inline fun <reified T> компилятор подставляет реальный класс в место вызова, и T::class работает — reified generics.

Вопрос. Зачем TypeScript generics, если типы всё равно исчезают?

Ответ. Они защищают на этапе разработки и в CI (tsc). Связь Promise<User> с телом async-функции, type-safe API и автодополнение в IDE — основная ценность — дженерики в TS.


Материалы по языкам

ЯзыкСтатья
C#Обобщения (generics)
JavaТипы и generics · Java 5
TypeScriptДженерики
GoДженерики в Go
PythonТипы · модуль typing
KotlinОбобщённые типы · reified
C++Язык C++
RustСправочник — generics
SwiftПротоколы и generics

Углублённый разбор Java

Type erasure по шагам

Компилятор Java выполняет примерно следующее.

  1. Проверяет типы в исходнике (list.add("x") в List<Integer> — ошибка).
  2. Заменяет T на границу (обычно Object или первый extends).
  3. Вставляет cast при чтении из generic-контейнера.
  4. Генерирует bridge methods, если сигнатура после erasure конфликтует с override.
// Исходник
class StringBox extends Box<String> {
String get() { ... }
}

// После erasure логически
class StringBox extends Box {
Object get() { ... } // bridge
String get() { ... } // синтетический мост
}

Почему нельзя new T()

В runtime T не существует. Компилятор не знает, какой конструктор вызвать. Обходные пути

  • передать Supplier<T> / Factory<T>;
  • передать Class<T> и clazz.getDeclaredConstructor().newInstance();
  • в Kotlin — reified T в inline.

Wildcards на практике

// Копировать числа из любого списка числовых типов
функция СкопироватьЧисла(источник: Список<? extends Число>, приёмник: Список<? super Число>)
для каждого n в источник
приёмник.добавить(n)
конец
КОНЕЦ

источник — producer (extends), приёмник — consumer (super).

Совместимость со старым кодом

// Код 2003 года
Список список = новый МассивСписок()
список.добавить("legacy")

// Код 2024 года
Список<Строка> современный = список // предупреждение unchecked

Raw type совместим с любым инстансом — источник предупреждений при смешении эпох.

Материалы — коллекции Java, рефлексия.


Углублённый разбор C#

Открытые и закрытые constructed types

ВидПримерВ runtime
ОткрытыйList<>Описание с параметром
ЗакрытыйList<int>Конкретный тип
Открытый generic-методvoid M<T>(T x)Метод с placeholder

Covariance и contravariance в C#

Только для делегатов и интерфейсов, не для классов.

IEnumerable<string> strings = ...;
IEnumerable<object> objects = strings; // covariant out

Action<object> actObj = ...;
Action<string> actStr = actObj; // contravariant in

List<string> нельзя присвоить List<object> — классы инвариантны, как в Java.

default(T)

Для value type — нули, для reference type — null. Удобно в generic-фабриках.

T? GetOrDefault<T>(T? value) => value ?? default;

Generic constraints и nullability

C# 8+ различает T (non-null по умолчанию для unconstrained) и T?. См. nullable reference types.


Углублённый разбор TypeScript

Связанные generic

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const name = getProperty(user, "name"); // string
const wrong = getProperty(user, "foo"); // ошибка компиляции

Тип K сужает допустимые ключи. Тип результата связан с ключом.

Utility types (краткий обзор)

ТипДействие
Partial<T>Все поля опциональны
Required<T>Все поля обязательны
Pick<T, K>Подмножество полей
Omit<T, K>Исключить поля
Record<K, V>Словарь ключ → значение
ReturnType<F>Тип возврата функции

Полный список — справочник utility types.

Conditional types

type IsString<T> = T extends string ? true : false;

Используются в библиотечных типах для ветвления по форме T. Продвинутый уровень — дженерики TS.


Углублённый разбор Go

Constraint interfaces

type Ordered interface {
~int | ~float64 | ~string
}

~int означает "int и именованные типы на базе int".

Когда интерфейс, когда generic

ЗадачаМеханизм
Разные типы с методом Read()io.Reader
Минимум двух Orderedfunc Min[T Ordered](a, b T) T
Хранение any в mapmap[string]any + type assertion

Пакеты stdlib

  • slicesContains, Sort, Clone с type parameters
  • mapsKeys, Values, Clone
  • cmpCompare, Less для ordered types

Дженерики в Go · интерфейсы.


Углублённый разбор Python

TypeVar с ограничениями

from typing import TypeVar

T = TypeVar("T")
Numeric = TypeVar("Numeric", int, float)

def double(x: Numeric) -> Numeric:
return x * 2

Protocol — structural subtyping

from typing import Protocol

class Drawable(Protocol):
def draw(self) -> None: ...

def render(shape: Drawable) -> None:
shape.draw()

Любой класс с методом draw подходит, без явного наследования — аналог constraints.

Generic классы

from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []

Проверка — mypy stack.py. См. typing, стиль кода.


Углублённый разбор C++

Шаблон функции

template<typename T>
const T& max(const T& a, const T& b) {
return a < b ? b : a;
}

Каждый вызов с новым T — новая функция в объектном файле (мономорфизация).

Concepts (C++20)

template<std::totally_ordered T>
const T& clamp(const T& v, const T& lo, const T& hi);

Ошибка "T не satisfies totally_ordered" понятнее, чем страница SFINAE.

Зависимые типы имён

Внутри шаблона typename T::iterator может быть типом или значением — ключевое слово typename уточняет. Источник путаницы у новичков в шаблонном коде.

О C++ · type traits.


Углублённый разбор Rust

Trait bounds

fn print_debug<T: std::fmt::Debug>(x: T) {
println!("{:?}", x);
}

Несколько bounds — T: Clone + Send + Sync.

Trait objects и generics

fn f<T: Trait>(x: T)fn f(x: &dyn Trait)
ДиспетчеризацияСтатическаяДинамическая
РазмерИзвестен на compile-timeУказатель + vtable
КогдаHot path, generic кодHeterogeneous коллекции

impl Trait в возврате

Скрывает конкретный тип, сохраняя статическую диспетчеризацию.

Справочник Rust · trait objects.


Кейс — каталог интернет-магазина

Доменные типы

СТРУКТУРА Товар
поле ид: Ид
поле название: Строка
поле цена: Деньги
КОНЕЦ

СТРУКТУРА Заказ
поле ид: Ид
поле позиции: Список<ПозицияЗаказа>
КОНЕЦ

Инфраструктура

ИНТЕРФЕЙС Репозиторий<T где T : Сущность>
метод Все(): Список<T>
метод Найти(ид: Ид): T?
КОНЕЦ

КЛАСС СервисКаталога
поле товары: Репозиторий<Товар>

метод Дорогие(порог: Деньги): Список<Товар>
вернуть Фильтр(товары.Все(), λ t → t.цена > порог)
КОНЕЦ
КОНЕЦ

СТРУКТУРА СтраницаТоваров
поле элементы: Список<Товар>
поле страница: Целое
поле всегоСтраниц: Целое
КОНЕЦ

API-слой возвращает Ответ<СтраницаТоваров>. Слой persistence работает с Репозиторий<Товар>. Тесты подменяют Репозиторий<Товар> на in-memory реализацию.

Связанные темы — ORM, веб-разработка.


Как читать ошибки компилятора

Java

incompatible types: String cannot be converted to Integer — неверный аргумент в generic-метод.

type argument X is not within bounds of type-variable T — нарушено extends.

C#

The type 'X' cannot be used as type parameter 'T' in the generic type or method — constraint не выполнен.

Cannot convert List<Dog> to List<Animal> — инвариантность.

C++

Ошибка на 50-й строке шаблона часто вызвана проблемой на 10-й. Стратегия

  • упростить вызов до минимального примера;
  • явно указать T вместо вывода;
  • вынести сложный тип в using Alias = ....

Rust

the trait bound X: Y is not satisfied — добавьте trait bound или impl.

size for values of type dyn Trait cannot be known at compilation time — нужен Box<dyn Trait> или generic.


Сравнение подходов к коллекциям

КритерийJava ArrayList<E>C# List<T>Python listGo sliceRust Vec<T>
Тип элементаgenericgenericаннотацияgeneric funcgeneric
Примитивычерез обёрткинапрямуюнативнонативнонативно
Runtime типerasurereifiedнетмономорфизациямономорфизация
Null в элементедопустимзависит от Tдопустимzero valueнет null без Option

Термины теории типов (расширение)

ТерминКратко
Параметрический полиморфизмОдин код, тип — параметр
Ad hoc полиморфизмПерегрузка по сигнатуре
Полиморфизм подтиповЗамена подтипом вместо базового
ИнвариантностьGeneric-контейнеры не ковариантны по умолчанию
Верхняя границаT extends U — T — подтип U
Нижняя граница? super T — принимает супертипы T
Kind"Тип типов" — в продвинутых системах (Haskell * -> *)
Higher-kinded typeF[_] — контейнер как параметр (Scala, Haskell)

Для прикладной разработки достаточно первых шести строк; остальное — при изучении Scala или Haskell.


Углублённый разбор Kotlin

Declaration-site variance

interface Producer<out T> {
fun produce(): T
}

interface Consumer<in T> {
fun consume(value: T)
}

out — ковариантный producer (только отдаёт T). in — контравариантный consumer (только принимает T).

Star projection

fun printSizes(list: List<*>) {
// элементы — неизвестный тип, но list.size доступен
}

List<*> — "список элементов неизвестного типа", безопаснее Java raw List.

Reified generics

inline fun <reified T> Gson.fromJson(json: String): T =
gson.fromJson(json, T::class.java)

Компилятор подставляет реальный класс в каждое место вызова inline-функции. Без inline + reified на JVM — только erasure.

Обобщённые типы · конструкции Kotlin.


Углублённый разбор Swift

Generic struct и class

struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element { items.removeLast() }
}

Where clauses

func sorted<C: Collection>(_ collection: C) -> [C.Element]
where C.Element: Comparable {
return collection.sorted()
}

Associated types в протоколах

protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
}

Item — тип, который определяет конкретная реализация. Связь с generics — протокол параметризует логику, associated type фиксирует элемент.

Протоколы Swift · ООП в Swift.


Схема проверки типов

На этапе CHECK компилятор отклоняет list.add("строка") для List<Integer>. На этапе ERASE выбирается стратегия языка — стирание, реификация или копии под каждый T.


Чек-лист перед code review

  • У всех коллекций указан аргумент типа
  • Нет лишних @SuppressWarnings("unchecked") / as any
  • Ограничения T соответствуют вызываемым методам
  • Wildcards / variance применены по PECS там, где передаём коллекции
  • Python — типы проверяются в CI (mypy/pyright)
  • TypeScript — границы API валидируются, не только типы
  • Публичный generic API документирован — что такое T, K, V
  • Тесты покрывают как минимум два разных аргумента типа для универсальных алгоритмов

Дополнительные материалы

Книги и стандарты

  • Java Generics and Collections (Bloch, Goetz) — erasure и коллекции
  • Effective C++ (Meyers) — шаблоны и STL
  • Rust Book — chapter on generics and traits

Википедия

В энциклопедии


Миграция с legacy-кода на обобщения

Типичный путь в Java-проектах, написанных до 2004 года или с унаследованными API.

Этап 1. Сырые коллекции

Список заказы = новый МассивСписок()
заказы.добавить(получитьЗаказ())
Заказ z = (Заказ) заказы.получить(0)

Риск — ClassCastException, нет подсказок IDE.

Этап 2. Добавление generic без изменения логики

Список<Заказ> заказы = новый МассивСписок<>()
заказы.добавить(получитьЗаказ())
Заказ z = заказы.получить(0)

Минимальное изменение — указать тип в объявлении и убрать cast.

Этап 3. Обобщение методов

// Было
функция НайтиЗаказ(список: Список, ид: Ид): Заказ
для каждого o в список
если ((Заказ)o).ид = ид
вернуть (Заказ)o
конец
конец
КОНЕЦ

// Стало
функция НайтиПоИд<T где T : Сущность>(список: Список<T>, ид: Ид): T?
для каждого o в список
если o.ид = ид
вернуть o
конец
конец
вернуть пусто
КОНЕЦ

Этап 4. Обобщение интерфейсов

// Было — отдельный репозиторий под каждую сущность
ИНТЕРФЕЙС РепозиторийЗаказов
метод Найти(ид: Ид): Заказ
КОНЕЦ

ИНТЕРФЕЙС РепозиторийПользователей
метод Найти(ид: Ид): Пользователь
КОНЕЦ

// Стало
ИНТЕРФЕЙС Репозиторий<T где T : Сущность>
метод Найти(ид: Ид): T?
КОНЕЦ

Совместимость при миграции

Старый байт-код вызывает новый generic API через bridge methods и raw types. Новый код не должен принимать raw types в публичном API. Стратегия

  • включить предупреждения компилятора об unchecked;
  • мигрировать модуль за модулем;
  • покрыть тестами критичные пути до замены cast.

См. основы Git для поэтапных коммитов рефакторинга.


Scala и Haskell (кратко)

В языках с сильной теорией типов generics — норма, а не дополнение.

Scala — JVM, erasure как в Java, но богатый синтаксис

  • class Box[A]
  • higher-kinded types F[_] в библиотеках (Cats, ZIO)
  • implicits / given для type classes

Scala — intro

Haskell — параметрический полиморфизм в основе

map :: (a -> b) -> [a] -> [b]

Тип a и b — параметры на уровне функции. Ошибки несовместимости — на этапе компиляции GHC.

Haskell

Для большинства прикладных задач на JVM, .NET или в веб-стеке достаточно материалов по Java, C#, TypeScript выше. Scala и Haskell полезны как расширение кругозора после освоения базовой статьи.


Обобщения в контрактах и микросервисах

В распределённых системах тип T часто живёт только в коде клиента и сервера, а по сети идёт JSON.

// Клиент (TypeScript)
тип ОтветПользователя = Ответ<Пользователь>
запрос(): Обещание<ОтветПользователя>

// Сервер (C#)
Task<ActionResult<UserDto>> GetUser(Guid id)

// Проверка на границе
валидатор.проверить(json, СхемаПользователя)

Роли generic-слоёв

  • внутри процессаRepository<T>, List<T>, type-safe шина событий;
  • на границе HTTP — DTO + схема (OpenAPI, JSON Schema);
  • между сервисами — контракт версионируется отдельно от T в коде.

OpenAPI Generator создаёт List<User> на клиенте из описания API. Generic в исходниках и контракт API — связанные, но не идентичные уровни.

См. микросервисы, интеграции.


Таблица «когда что применять»

СитуацияРекомендация
Список однотипных объектовList<T> / Vec<T> / []T
Кэш по строковому ключуMap<string, T> / Dict<K,V>
Универсальная сортировкаgeneric-функция + Comparable / Ordered
Разные типы с общим интерфейсоминтерфейс + подтипы (ООП)
Разные типы, общий алгоритм без иерархииgeneric + constraint
Тип заранее неизвестен, рефлексияany / interface{} — последний resort
Десериализация JSON в List<T>TypeToken / TypeReference / валидация схемы
Публичная библиотекаgenerics + документированные bounds
Внутренний скрипт на 50 строкконкретные типы допустимы

Повторение и закрепление

Параметр типа — заглушка T в объявлении. Аргумент типа — конкретный тип при использовании (Order в List<Order>).

Три причины использовать обобщения

  • безопасность типов на этапе компиляции;
  • один алгоритм для многих типов (DRY);
  • меньше ручных приведений.

Три вида полиморфизма — параметрический (generics), подтипы (наследование), ad hoc (перегрузка).

Три стратегии runtime

  • стирание (Java);
  • реификация (C#);
  • мономорфизация (Rust, Go, C++).

Variance

  • инвариантность — List<Dog> нельзя в List<Animal>;
  • ковариантность — чтение (extends / out);
  • контравариантность — запись (super / in);
  • PECS в Java — producer extends, consumer super.

Ограничения — сужают T и дают вызывать методы интерфейса внутри generic-кода.

Сырой типList без <T>; избегать в новом коде.

Reified — тип в runtime (C# всегда, Kotlin в inline reified).

Практика


Шпаргалка синтаксиса

КонструкцияJavaC#TypeScriptGoRustKotlinPython
Классclass Box<T>class Box<T>class Box<T>type Box[T any] structstruct Box<T>class Box<T>class Box(Generic[T])
Метод<T> T id(T x)T Id<T>(T x)function id<T>(x: T)func Id[T any](x T) Tfn id<T>(x: T) -> Tfun <T> id(x: T): Tdef id(x: T) -> T
Ограничение<T extends Comparable<T>>where T : IComparable<T><T extends { id: string }>[T cmp.Ordered]T: Ord + Clone<T : Comparable<T>>T = TypeVar("T", bound=...)
Wildcard / variance? extends Tout T в интерфейсеtrait boundsout T / in TCovariant (редко)
Созданиеnew ArrayList<>()new List<int>()new Array<T>()Stack[int]{}Vec::<T>::new()mutableListOf<T>()list[T]()
Reified / runtime TClass<T>typeof(T)нет в JSreflectTypeIdreified T inlineget_args

Итог

Обобщения дают один типобезопасный шаблон вместо множества копий под каждый тип. Ошибки несовместимости ловятся при компиляции или статическом анализе. Это параметрический полиморфизм — рядом с полиморфизмом подтипов и перегрузкой, но отдельный инструмент. Синтаксис и поведение в runtime зависят от языка; детали — в разделах по языкам и таблице материалов.

Содержание