Я изучаю дженерики Go 1.18 и пытаюсь понять, почему у меня здесь проблемы. Короче говоря, я пытаюсь Unmarshal
создать protobuf и хочу, чтобы тип параметра в blah
«просто работал». Я максимально упростил проблему, и этот конкретный код воспроизводит то же сообщение об ошибке, которое я вижу:
./prog.go:31:5: cannot use t (variable of type *T) as type stringer in argument to do:
*T does not implement stringer (type *T is pointer to type parameter, not type parameter)
package main
import "fmt"
type stringer interface {
a() string
}
type foo struct{}
func (f *foo) a() string {
return "foo"
}
type bar struct{}
func (b *bar) a() string {
return "bar"
}
type FooBar interface {
foo | bar
}
func do(s stringer) {
fmt.Println(s.a())
}
func blah[T FooBar]() {
t := &T{}
do(t)
}
func main() {
blah[foo]()
}
Я понимаю, что могу полностью упростить этот пример, не используя дженерики (т. е. передать экземпляр в blah(s stringer) {do(s)}
). Однако я хочу понять, почему возникает ошибка.
Что мне нужно изменить в этом коде, чтобы я мог создать экземпляр T
и передать этот указатель функции, ожидающей определенной подписи метода?
https://golang.google.cn/blog/intro-generics
Until recently, the Go spec said that an interface defines a method set, which is roughly the set of methods enumerated in the interface. Any type that implements all those methods implements that interface.
package main
import "fmt"
type stringer interface {
a() string
}
type foo struct{}
func (f foo) a() string {
return "foo"
}
type bar struct{}
func (b bar) a() string {
return "bar"
}
type FooBar interface {
stringer
}
func do(s stringer) {
fmt.Println(s.a())
}
func blah[T FooBar]() {
t := new(T)
do(*t)
}
func main() {
blah[foo]()
}
и вы можете удалить тип FooBar
, потому что FooBar (Generics) такой же, как stringer (интерфейс)
В вашем коде нет никакой связи между ограничениями FooBar
и stringer
. Кроме того, методы реализованы на приемниках указателей.
Быстрое и грязное исправление для вашей надуманной программы — это просто утверждать, что *T
действительно stringer
:
func blah[T FooBar]() {
t := new(T)
do(any(t).(stringer))
}
Детская площадка: https://go.dev/play/p/zmVX56T9LZx
Но это нарушает безопасность типов и может привести к панике во время выполнения. Чтобы сохранить безопасность типа времени компиляции, другим решением, которое несколько сохраняет семантику ваших программ, будет следующее:
type FooBar[T foo | bar] interface {
*T
stringer
}
func blah[T foo | bar, U FooBar[T]]() {
var t T
do(U(&t))
}
Так что же здесь происходит?
Во-первых, связь между параметром типа и его ограничением не является тождеством: T
не являетсяFooBar
. Вы не можете использовать T
так, как это было FooBar
, поэтому *T
определенно не эквивалентно *foo
или *bar
.
Поэтому, когда вы вызываете do(t)
, вы пытаетесь передать тип *T
во что-то, что ожидает stringer
, но T
, указатель или нет, просто не имеет метода a() string
в своем наборе типов.
Шаг 1: добавьте метод a() string
в интерфейс FooBar
(путем встраивания stringer
):
type FooBar interface {
foo | bar
stringer
}
Но этого еще недостаточно, потому что теперь ни один из ваших типов на самом деле не реализует это. Оба объявляют метод для получателя указателя.
Шаг 2: измените типы в объединении на указатели:
type FooBar interface {
*foo | *bar
stringer
}
Теперь это ограничение работает, но у вас есть другая проблема. Вы не можете объявить составные литералы, если ограничение не имеет основного типа. Так что t := T{}
тоже недействителен. Мы меняем его на:
func blah[T FooBar]() {
var t T // already pointer type
do(t)
}
Теперь это компилируется, но t
на самом деле является нулевым значением типа указателя, так что это nil
. Ваша программа не падает, потому что методы просто возвращают некоторый строковый литерал.
Если вам нужно также инициализировать память, на которую ссылаются указатели, внутри blah
вам нужно знать о базовых типах.
Шаг 3: Таким образом, вы добавляете T foo | bar
в качестве параметра одного типа и меняете подпись на:
func blah[T foo | bar, U FooBar]() {
var t T
do(U(&t))
}
Готово? Еще нет. Преобразование U(&t)
по-прежнему недействительно, поскольку набор типов U
и T
не совпадает. Теперь вам нужно параметризовать FooBar
в T
.
Шаг 4: в основном вы извлекаете объединение FooBar
в параметр типа, так что во время компиляции его набор типов будет включать только один из двух типов:
type FooBar[T foo | bar] interface {
*T
stringer
}
Ограничение теперь можно создать с помощью T foo | bar
, сохранить безопасность типов, семантику указателя и инициализировать T
ненулевым.
func (f *foo) a() string {
fmt.Println("foo nil:", f == nil)
return "foo"
}
func main() {
blah[foo]()
}
Отпечатки:
foo nil: false
foo
Детская площадка: https://go.dev/play/p/src2sDSwe5H
Если вы можете создать экземпляр blah
с типами указателей или, что еще лучше, передать ему аргументы, вы можете удалить все промежуточные хитрости:
type FooBar interface {
*foo | *bar
stringer
}
func blah[T FooBar](t T) {
do(t)
}
func main() {
blah(&foo{})
}
Можете ли вы также предоставить площадку для «быстрого и грязного» исправления?
@TimReddy Ну вот, однако при написании игровой площадки я понял две вещи: 1) это не «быстро», на самом деле это почти то же самое, что и более длинное решение с FooBar
; 2) он выдает ошибку компоновщика... оставляя его просто "грязным". На самом деле быстрым решением может быть это, но я чувствую, что оно надумано до такой степени, что бесполезно. Я уберу это замечание из ответа
К вашему сведению, ошибка кажется исправленной на кончике
@TimReddy Я добавил на самом деле быстрый и грязное решение обратно в ответ. Я все еще рекомендую пойти с более длинным
В вашем примере указатель удаляется из метода получателя.