Нельзя использовать переменную типа *T в качестве типа аргумента

Я изучаю дженерики 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 и передать этот указатель функции, ожидающей определенной подписи метода?

Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
4
0
82
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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 (интерфейс)

В вашем примере указатель удаляется из метода получателя.

Tim Reddy 08.04.2022 18:05
Ответ принят как подходящий

В вашем коде нет никакой связи между ограничениями 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{})
}

Можете ли вы также предоставить площадку для «быстрого и грязного» исправления?

Tim Reddy 08.04.2022 18:16

@TimReddy Ну вот, однако при написании игровой площадки я понял две вещи: 1) это не «быстро», на самом деле это почти то же самое, что и более длинное решение с FooBar; 2) он выдает ошибку компоновщика... оставляя его просто "грязным". На самом деле быстрым решением может быть это, но я чувствую, что оно надумано до такой степени, что бесполезно. Я уберу это замечание из ответа

blackgreen 08.04.2022 21:25

К вашему сведению, ошибка кажется исправленной на кончике

blackgreen 08.04.2022 21:30

@TimReddy Я добавил на самом деле быстрый и грязное решение обратно в ответ. Я все еще рекомендую пойти с более длинным

blackgreen 09.04.2022 18:57

Другие вопросы по теме