Как ограничение типа определяет, допустим ли конкретный тип аргумента обобщённой функции в Go?
Конкретный тип можно передать в обобщённую функцию, если он входит в множество типов, заданное ограничением параметра типа. Ограничение может требовать методы, перечислять конкретные типы или допускать типы с определённым базовым типом через ~.
Проверка выполняется на этапе компиляции: компилятор одновременно проверяет допустимость типа-аргумента и доступность операций, разрешённых его ограничением.
Обобщения появились в Go 1.18, чтобы повторно использовать алгоритмы для разных типов без отказа от статической типизации и без необходимости писать отдельные версии функций либо прибегать к значениям interface{}.
Ограничения типов решают проблему слишком широкого обобщения: функция получает не любой тип, а только типы, для которых её операции корректны. Благодаря этому ошибки обнаруживаются до запуска программы.
Допустим, функция должна складывать значения. Ограничение только интерфейсом без подходящего множества типов не гарантирует, что оператор сложения применим к каждому возможному аргументу.
Слишком узкое ограничение уменьшает повторное использование функции, а слишком широкое не позволяет компилятору подтвердить корректность операций. Отдельно важно различать точное указание типа int и запись ~int: первая допускает только int, вторая — также именованные типы с базовым типом int.
Ограничение параметра типа — это интерфейс, используемый как описание допустимых типов. Его множество типов формируется из элементов интерфейса: методов, конкретных типов и объединений типов.
В этом примере int и Score допустимы, потому что их базовый тип — int; float64 также входит в множество типов ограничения. Строка или структура не подойдут, поскольку не входят в это множество, поэтому программа не скомпилируется.
Оператор + разрешён внутри функции только потому, что он применим ко всем типам, представленным ограничением ~int | ~float64. Если операция не имеет смысла для каждого типа множества, её использование запрещается компилятором.
Методы в ограничении работают иначе: они задают поведение, которое должен предоставлять тип. Например, ограничение с методом допускает типы, реализующие этот метод, но само по себе не перечисляет конкретные типы. При вызове компилятор обычно выводит аргумент типа из обычных аргументов функции; если вывести его невозможно, тип указывают явно.
Компромисс состоит в том, что ограничения повышают безопасность и позволяют использовать типоспецифичные операции, но усложняют API. Слишком сложное объединение типов может сделать код менее читаемым и ограничить набор допустимых операций.
В библиотеке обработки числовых метрик сначала использовали interface{} и проверяли тип значения во время выполнения. Такой вариант принимал любые данные, но ошибки обнаруживались поздно, а реализация требовала ветвлений и преобразований типов.
Рассматривались два решения: написать отдельные функции для int и float64 либо ввести обобщённую функцию с ограничением. Отдельные функции проще для новичка, но создают дублирование; interface{} гибче, но переносит проверку на время выполнения.
Выбрали обобщённую функцию с ограничением числовых типов. В результате недопустимые типы отсекаются компилятором, алгоритм остаётся единым, а именованные числовые типы поддерживаются благодаря ~. Ограничение при этом оставили небольшим, чтобы не усложнять публичный API.
int и ~int в ограничении?int обозначает точное множество, состоящее из типа int. ~int включает int и именованные типы, чей базовый тип — int, например тип, объявленный как отдельный именованный тип на основе int. Это не означает, что подходят произвольные типы, имеющие похожее представление.
Нет. Операция должна быть допустима для всего множества типов, описанного ограничением. Если объединение содержит типы, для которых оператор недоступен или имеет несовместимую семантику, компилятор не разрешит такую операцию над параметром типа.
Нет. Вывод работает, когда тип параметра можно однозначно определить по обычным аргументам или другим контекстным данным, поддерживаемым вызовом. Если параметр типа не связан с аргументами либо вывод неоднозначен, его нужно указать явно; само ограничение обычно не выбирает единственный конкретный тип.