При передаче именованного типа в обобщённую функцию сохраняет ли параметр типа имя исходного типа или использует его базовый тип?
Параметр типа обычно получает фактический именованный тип, переданный в аргументе, а не его базовый тип. Если передать значение типа UserID, объявленного на основе int, то результат с типом T также будет иметь тип UserID.
Базовый тип используется для проверки ограничений вроде ~int, но не заменяет фактический типовой аргумент.
Обобщения в Go появились для написания алгоритмов, работающих с разными типами без потери типобезопасности. Важной частью этого подхода стало сохранение информации о конкретном типе, включая именованные типы, чтобы generic-код не превращал их автоматически в базовые типы.
Это позволяет использовать общую реализацию и одновременно сохранять методы, ограничения и семантику именованного типа на стороне вызывающего кода.
Рассмотрим тип идентификатора, основанный на int. Если generic-функция превратит его в int, то результат потеряет смысловую типизацию: компилятор больше не сможет отличать идентификатор от обычного числа.
Такое неявное преобразование повысило бы риск смешивания несовместимых значений. Поэтому нужно различать именованный тип, базовый тип и типовой аргумент, выбранный при инстанцировании функции.
При выводе параметров типа компилятор использует тип аргумента. Для значения именованного типа UserID параметр T выводится как UserID, а не как int.
Ограничение ~int означает, что допустимы типы с базовым типом int, включая UserID. Однако оно только описывает множество разрешённых типов; оно не заставляет компилятор подменять UserID на int.
Это отличается от явного преобразования. Если результат привести к int, именованный тип будет потерян намеренно. При обычной передаче в generic-функцию тип сохраняется, что важно для присваиваний, методов и дальнейших проверок типов.
В библиотеке есть тип OrderID, основанный на int, и универсальная функция форматирования значений. Разработчик мог бы объявить функцию только для int, но тогда пришлось бы явно преобразовывать каждый OrderID, а результат перестал бы сохранять доменный тип.
Вариант с ограничением ~int принимает и int, и OrderID. Его плюс — повторное использование кода без потери типобезопасности; минус — функция намеренно допускает все типы на основе int, даже если бизнес-смысл у них различается. Если нужен только один доменный тип, лучше ограничить API конкретным OrderID.
Выбранный вариант с параметром типа оправдан для общей числовой операции: вычисление выполняется над значением, а возвращаемый результат сохраняет исходный именованный тип. В итоге вызов не требует преобразований, а компилятор продолжает различать OrderID и обычный int.
Дополнительный вопрос 1. Что произойдёт, если в generic-функцию передать нетипизированную целочисленную константу?
Нетипизированная константа сначала получает подходящий конкретный тип с учётом контекста и ограничения. Например, при ограничении ~int параметр типа может быть выведен как int; именованный тип здесь не возникает сам по себе, потому что у константы изначально нет имени пользовательского типа.
Дополнительный вопрос 2. Меняется ли вывод типа при явном указании типового аргумента?
Да, вызывающий код может явно выбрать типовой аргумент. Если передать значение UserID, но явно указать int, компилятор проверит возможность использования значения как int, а результат будет иметь тип int. Это уже не автоматический вывод, а заданный разработчиком выбор с соответствующей потерей именованного типа.
Дополнительный вопрос 3. Достаточно ли совпадения базового типа, чтобы любой именованный тип подошёл ограничению int без тильды?
Нет. Ограничение int обозначает конкретный тип int, тогда как ~int включает именованные типы, чей базовый тип — int. Поэтому UserID удовлетворяет ~int, но не обязательно удовлетворяет ограничению, содержащему только точный тип int.