В обобщённой функции нужно принять как встроенный int, так и собственный тип с базовым типом int. Объясните, почему этот вызов допустим:
package main
type UserID int
type Signed interface {
~int | ~int64
}
func add[T Signed](a, b T) T {
return a + b
}
func main() {
var id UserID = 10
_ = add(id, UserID(2))
}
Вызов допустим, потому что ~int включает не только точный тип int, но и все именованные типы, чей базовый тип — int. Поэтому UserID удовлетворяет ограничению Signed, а результат add сохраняет тип UserID.
Если заменить ~int на int, тип UserID больше не подойдёт: без ~ ограничение перечисляет только сам тип int.
Обобщённые функции должны уметь работать не только со встроенными типами, но и с пользовательскими типами, которые имеют ту же базовую структуру и поддерживают те же операции. Например, UserID логически отличается от int, однако арифметика сложения для него определяется базовым типом.
Оператор ~ позволяет описать такое ограничение без потери типобезопасности: функция не принимает произвольное значение, а только типы с указанным базовым типом.
Без ~ разработчик вынужден либо перечислять только встроенный тип, либо писать отдельные функции для каждого именованного типа. Это ухудшает повторное использование кода и может привести к явным преобразованиям, которые усложняют API.
При этом чрезмерно широкое ограничение тоже опасно: оно может разрешить типы, для которых операция или смысл вычисления нежелательны. Поэтому ограничение должно описывать именно допустимое множество типов.
В объявлении
~int означает множество всех типов, чей underlying type, то есть базовый тип, равен int. В это множество входят int и UserID, объявленный как type UserID int. Аналогично, ~int64 включает int64 и именованные типы на его основе.
Запись int без ~ означает только точный тип int. Например, при таком ограничении вызов с UserID не скомпилировался бы:
Ограничение также определяет доступные операции над параметром типа. Для всех типов из Signed операция + допустима, поэтому тело add корректно. Параметр типа T сохраняется: при вызове с UserID функция возвращает UserID, а не автоматически преобразованный int.
Символ ~ не означает неявное преобразование значения. Он участвует только в проверке соответствия типа ограничению на этапе компиляции. Значения разных именованных типов по-прежнему нельзя складывать напрямую, если они не имеют одного конкретного типа параметра.
В библиотеке нужно сложить идентификаторы разных доменов: UserID, OrderID и ProductID. Все они объявлены на основе int, но являются разными типами, чтобы компилятор не позволял случайно передать идентификатор заказа туда, где ожидается идентификатор пользователя.
Можно сделать отдельные функции для каждого типа. Это сохраняет строгую типизацию, но приводит к дублированию кода и усложняет сопровождение. Можно принимать int, но тогда придётся явно преобразовывать идентификаторы, а API потеряет информацию о доменном типе.
Выбранное решение — обобщённая функция с ограничением ~int, если операция действительно корректна для всех таких типов. Она сохраняет тип результата и принимает пользовательские типы без преобразований. Если же разные идентификаторы нельзя смешивать по смыслу, одну общую арифметическую функцию применять не следует: техническая совместимость базовых типов не гарантирует бизнес-корректность операции.
Вопрос: Включает ли ~int тип type UserID = int?
Ответ: Да, но по другой причине: это псевдоним, а не новый именованный тип. UserID и int при таком объявлении являются одним и тем же типом. Для нового типа type UserID int соответствие обеспечивается именно правилом ~int.
Вопрос: Можно ли заменить ~int | ~int64 на ~int | ~int64 | ~float64, если функция использует +?
Ответ: Да, операция + допустима для всех перечисленных числовых типов. Однако это меняет контракт функции: она начнёт принимать типы с плавающей точкой, а результат может иметь дробную часть и другие числовые свойства. Ограничение должно отражать не только наличие оператора, но и смысл операции.
Вопрос: Что произойдёт, если передать в add значения типов UserID и int?
Ответ: Такой вызов не будет принят, если компилятор должен вывести один параметр T: аргументы имеют разные конкретные типы. То, что оба типа входят в множество Signed, не делает их взаимозаменяемыми. Нужно явно выбрать единый тип и преобразовать один из аргументов, например add(id, UserID(2)) или add(int(id), 2).