无法使用 *T 类型的变量作为参数中的类型

Tim*_*ddy 6 generics go

我正在学习 Go 1.18 泛型,我试图理解为什么我在这里遇到麻烦。长话短说,我正在尝试使用Unmarshalprotobuf,并且希望参数类型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)
Run Code Online (Sandbox Code Playgroud)
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]()
}
Run Code Online (Sandbox Code Playgroud)

我意识到我可以通过不使用泛型来完全简化这个示例(即将实例传递给blah(s stringer) {do(s)})。但是,我确实想了解为什么会发生错误。

我需要对此代码进行哪些更改,以便我可以创建一个实例T并将该指针传递给需要特定方法签名的函数?

bla*_*een 8

FooBar在您的代码中,约束和之间没有关系stringer。此外,这些方法是在指针接收器上实现的。

对你设计的程序的一个快速而肮脏的修复就是简单地断言*T确实是一个stringer

func blah[T FooBar]() {
    t := new(T)
    do(any(t).(stringer))
}
Run Code Online (Sandbox Code Playgroud)

游乐场: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))
}
Run Code Online (Sandbox Code Playgroud)

那么这是怎么回事呢?

首先,类型参数与其约束之间的关系不是identity:T 不是 FooBar。你不能T像以前那样使用FooBar,因此*T绝对不等于*fooor *bar

因此,当您调用 时do(t),您正在尝试将类型*T传递到需要 的对象中stringer,但是,无论是否为指针,其类型集中T本质上都没有该方法。a() string

步骤1:将方法添加到接口a() stringFooBar(通过嵌入stringer):

type FooBar interface {
    foo | bar
    stringer
}
Run Code Online (Sandbox Code Playgroud)

但这还不够,因为现在没有一个类型真正实现它。两者都在指针接收器上声明了方法。

步骤2:将union中的类型更改为指针:

type FooBar interface {
    *foo | *bar
    stringer
}
Run Code Online (Sandbox Code Playgroud)

这个约束现在有效,但你还有另一个问题。当约束没有核心类型时,您无法声明复合文字。所以t := T{}也是无效的。我们将其更改为:

func blah[T FooBar]() {
    var t T // already pointer type
    do(t)
}
Run Code Online (Sandbox Code Playgroud)

现在可以编译了,但t实际上是指针类型的零值,所以它是nil. 您的程序不会崩溃,因为这些方法只返回一些字符串文字。

如果您还需要初始化指针引用的内存,那么blah您需要了解内部的基本类型。

第 3 步:因此,您添加T foo | bar一种类型参数,并将签名更改为:

func blah[T foo | bar, U FooBar]() {
    var t T
    do(U(&t))
}
Run Code Online (Sandbox Code Playgroud)

完毕?还没有。转换U(&t)仍然无效,因为两者的类型集UT匹配。您现在需要FooBar在 中进行参数化T

步骤 4:基本上,您将FooBar的 union 提取到类型参数中,以便在编译时其类型集将仅包含以下两种类型之一:

type FooBar[T foo | bar] interface {
    *T
    stringer
}
Run Code Online (Sandbox Code Playgroud)

现在可以使用 实例化约束T foo | bar,保留类型安全、指针语义并初始化T为非零。

func (f *foo) a() string {
    fmt.Println("foo nil:", f == nil)
    return "foo"
}

func main() {
    blah[foo]()
}
Run Code Online (Sandbox Code Playgroud)

印刷:

foo nil: false
foo
Run Code Online (Sandbox Code Playgroud)

游乐场: 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{})
}
Run Code Online (Sandbox Code Playgroud)