Go语言 无法将 *T类型的变量用作参数中的类型

vjrehmav  于 2023-09-28  发布在  Go
关注(0)|答案(2)|浏览(146)

我正在学习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的示例,并将该指针传递给需要特定方法签名的函数?

pepwfjgg

pepwfjgg1#

在您的代码中,约束FooBarstringer之间没有关系。此外,所述方法在指针接收器上实施。
对于你的程序,一个快速而肮脏的修复方法是简单地 * Assert * *T确实是一个stringer

func blah[T FooBar]() {
    t := new(T)
    do(any(t).(stringer))
}

Playground: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。您不能像使用FooBar那样使用T,因此*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)仍然无效,因为UT的类型集不匹配。现在需要在T中参数化FooBar
第4步:基本上,您将FooBar的union提取到一个类型参数中,以便在编译时其类型集将仅包含以下两种类型中的一种:

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

Playground: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{})
}
hwamh0ep

hwamh0ep2#

除了上面的答案,还有两种方法可以解决这个问题。在这两种情况下,FooBar类型必须是:

type FooBar interface {
    *foo | *bar
    stringer
}

第一个:

创建此通用New函数。它与内置的new func功能相同,但可以轻松地作为函数参数传递。

func New[T any]() *T {
    return new(T)
}

修改blah函数以接受并使用_new函数。此函数负责创建FooBar示例。

func blah[T FooBar](_new func() T) {
    t := _new()
    do(t)
}

修改主函数:

func main() {
    blah(New[foo])
}

基本上,我们将内置的新函数注入到泛型函数中。
Full solution on a go playground

第二个:

我们可以使用反射来示例化隐藏在接口后面的指针类型的变量:

func gnew[T any]() T {
    var t T
    var _T = reflect.TypeOf(t)
    if _T.Kind() == reflect.Pointer {
        return reflect.New(_T.Elem()).Interface().(T)
    }
    return t
}

blah和main函数是:

func blah[T FooBar]() {
    var t = gnew[T]()
    do(t)
}

func main() {
    blah[*foo]()
}

Full solution on a go playground

相关问题