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

huangapple go评论79阅读模式
英文:

Cannot use variable of type *T as type in argument

问题

我正在学习Go 1.18的泛型,并且我试图理解为什么我在这里遇到了问题。简而言之,我正在尝试对protobuf进行Unmarshal,并且我希望在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的实例并将该指针传递给期望特定方法签名的函数?

英文:

I'm learning Go 1.18 generics and I'm trying to understand why I'm having trouble here. Long story short, I'm trying to Unmarshal a protobuf and I want the parameter type in blah to "just work". I've simplified the problem as best I could, and this particular code is reproducing the same error message I'm seeing:

./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]()
}

I realize that I can completely simplify this example by not using generics (i.e., pass the instance to blah(s stringer) {do(s)}. However, I do want to understand why the error is happening.

What do I need to change with this code so that I can create an instance of T and pass that pointer to a function expecting a particular method signature?

答案1

得分: 8

在你的代码中,FooBarstringer之间没有关联。此外,这些方法是在指针接收器上实现的。

对于你的这个例子,一个快速而简单的修复方法是断言 *T 确实是一个 stringer

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

Playground: https://go.dev/play/p/zmVX56T9LZx

但是这种方法放弃了类型安全,并且可能在运行时出现 panic。为了保持编译时的类型安全,另一种解决方案是在一定程度上保留程序的语义:

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 // 已经是指针类型
	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 的联合类型提取为一个类型参数,这样在编译时,它的类型集将只包含两个类型中的一个:

type FooBar[T foo | bar] interface {
    *T
    stringer
}

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

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{})
}
英文:

In your code there's no relationship between the constraints FooBar and stringer. Furthermore the methods are implemented on the pointer receivers.

A quick and dirty fix for your contrived program is simply to assert that *T is indeed a stringer:

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

Playground: https://go.dev/play/p/zmVX56T9LZx

But this forgoes type safety, and could panic at run time. To preserve compile time type safety, another solution that somewhat preserves your programs' semantics would be this:

type FooBar[T foo | bar] interface {
	*T
	stringer
}

func blah[T foo | bar, U FooBar[T]]() {
    var t T
	do(U(&t))
}

So what's going on here?

First, the relationship between a type parameter and its constraint is not identity: T is not FooBar. You cannot use T like it was FooBar, therefore *T is definitely not equivalent to *foo or *bar.

So when you call do(t), you're attempting to pass a type *T into something that expects a stringer, but T, pointer or not, just does not inherently have the a() string method in its type set.

Step 1: add the method a() string into the FooBar interface (by embedding stringer):

type FooBar interface {
    foo | bar
    stringer
}

But that's not enough yet, because now none of your types actually implement it. Both declare the method on the pointer receiver.

Step 2: change the types in the union to be pointers:

type FooBar interface {
    *foo | *bar
    stringer
}

This constraint now works, but you have another problem. You can't declare composite literals when the constraint doesn't have a core type. So t := T{} is also invalid. We change it to:

func blah[T FooBar]() {
	var t T // already pointer type
	do(t)
}

Now this compiles, but t is actually the zero value of a pointer type, so it's nil. Your program doesn't crash because the methods just return some string literal.

If you need to also initialize the memory referenced by the pointers, inside blah you need to know about the base types.

Step 3: So you add T foo | bar as one type param, and change the signature to:

func blah[T foo | bar, U FooBar]() {
    var t T
    do(U(&t))
}

Done? Not yet. The conversion U(&t) is still invalid because the type set of both U and T don't match. You need to now parametrize FooBar in T.

Step 4: basically you extract FooBar's union into a type param, so that at compile time its type set will include only one of the two types:

type FooBar[T foo | bar] interface {
    *T
    stringer
}

The constraint now can be instantiated with T foo | bar, preserve type safety, pointer semantics and initialize T to non-nil.

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

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

Prints:

foo nil: false
foo

Playground: https://go.dev/play/p/src2sDSwe5H

<hr>

If you can instantiate blah with pointer types, or even better pass arguments to it, you can remove all the intermediate trickery:

type FooBar interface {
    *foo | *bar
    stringer
}

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

func main() {
	blah(&amp;foo{})
}

huangapple
  • 本文由 发表于 2022年4月8日 12:20:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/71791637.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定