Go泛型 – 联合类型

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

Go Generics - Unions

问题

我正在玩弄go泛型,通过修改我创建的用于处理切片的库。我有一个Difference函数,它接受切片并返回仅在其中一个切片中找到的唯一元素的列表。

我修改了这个函数以使用泛型,并尝试使用不同类型(例如字符串和整数)编写单元测试,但在联合类型方面遇到了问题。现在是我的代码:

type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
	input testDifferenceInput[T]
	output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
	for i, tt := range []testDifference[int] {
		testDifference[int]{
			input: testDifferenceInput[int]{
				[]int{1, 2, 3, 3, 4},
				[]int{1, 2, 5},
				[]int{1, 3, 6},
			},
			output: []int{4, 5, 6},
		},
	} {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			actual := Difference(tt.input...)

			if !isEqual(actual, tt.output) {
				t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
			}
		})
	}
}

我想能够在同一个表测试中测试整数和字符串。这是我尝试过的:

type intOrString interface {
	int | string
}
type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
	input testDifferenceInput[T]
	output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
	for i, tt := range []testDifference[intOrString] {
		testDifference[int]{
			input: testDifferenceInput[int]{
				[]int{1, 2, 3, 3, 4},
				[]int{1, 2, 5},
				[]int{1, 3, 6},
			},
			output: []int{4, 5, 6},
		},
		testDifference[string]{
			input: testDifferenceInput[string]{
				[]string{"1", "2", "3", "3", "4"},
				[]string{"1", "2", "5"},
				[]string{"1", "3", "6"},
			},
			output: []string{"4", "5", "6"},
		},
	} {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			actual := Difference(tt.input...)

			if !isEqual(actual, tt.output) {
				t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
			}
		})
	}
}

然而,运行时我得到以下错误:

$ go version
go version dev.go2go-55626ee50b linux/amd64

$ go tool go2go test
arrayOperations_unit_test.go2:142:6: expected ';', found '|' (and 5 more errors)

为什么它抱怨我的intOrString接口?

编辑 #1 - 我可以确认,在@Nulo的帮助下,gotip确实可以工作,我现在明白为什么我不能使用intOrString作为类型 - 它应该是一个约束。

然而,找到一种在我的表测试中混合整数和字符串的方法仍然是不错的...

$ gotip version
go version devel go1.18-c812b97 Fri Oct 29 22:29:31 2021 +0000 linux/amd64

$ gotip test
# github.com/adam-hanna/arrayOperations/go2 [github.com/adam-hanna/arrayOperations/go2.test]
./arrayOperations_unit_test.go:152:39: interface contains type constraints
./arrayOperations_unit_test.go:152:39: intOrString does not satisfy intOrString
./arrayOperations_unit_test.go:155:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:156:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:157:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:159:13: incompatible type: cannot use []int{} (value of type []int) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:163:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:164:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:165:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:167:13: incompatible type: cannot use []string{} (value of type []string) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:152:39: too many errors
FAIL    github.com/adam-hanna/arrayOperations/go2 [build failed]
英文:

I'm playing around with go generics by modifying a library I created for working with slices. I have a Difference function which accepts slices and returns a list of unique elements only found in one of the slices.

I modified the function to use generics and I'm trying to write unit tests with different types (e.g. strings and ints) but am having trouble with the union type. Here's what I have, now:

type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
	input testDifferenceInput[T]
	output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
		for i, tt := range []testDifference[int] {
			testDifference[int]{
				input: testDifferenceInput[int]{
					[]int{1, 2, 3, 3, 4},
					[]int{1, 2, 5},
					[]int{1, 3, 6},
				},
				output: []int{4, 5, 6},
			},
		} {
			t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
				actual := Difference(tt.input...)

				if !isEqual(actual, tt.output) {
					t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
				}
		})
	}
}

I would like to be able to test both int's or string's in the same table test. Here's what I've tried:

type intOrString interface {
	int | string
}
type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
	input testDifferenceInput[T]
	output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
		for i, tt := range []testDifference[intOrString] {
			testDifference[int]{
				input: testDifferenceInput[int]{
					[]int{1, 2, 3, 3, 4},
					[]int{1, 2, 5},
					[]int{1, 3, 6},
				},
				output: []int{4, 5, 6},
			},
			testDifference[string]{
				input: testDifferenceInput[string]{
					[]string{"1", "2", "3", "3", "4"},
					[]string{"1", "2", "5"},
					[]string{"1", "3", "6"},
				},
				output: []string{"4", "5", "6"},
			},
		} {
			t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
				actual := Difference(tt.input...)

				if !isEqual(actual, tt.output) {
					t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
				}
		})
	}
}

However, when running this, I get the following error:

$ go version
go version dev.go2go-55626ee50b linux/amd64

$ go tool go2go test
arrayOperations_unit_test.go2:142:6: expected ';', found '|' (and 5 more errors)

Why is it complaining about my intOrString interface?

EDIT #1 - I can confirm, with @Nulo's help, that gotip does work, and I now understand why I can't use intOrString as a type - it's supposed to be a constraint.

However, it would still be nice to find some way to mix ints and strings in my table test...

$ gotip version
go version devel go1.18-c812b97 Fri Oct 29 22:29:31 2021 +0000 linux/amd64

$ gotip test
# github.com/adam-hanna/arrayOperations/go2 [github.com/adam-hanna/arrayOperations/go2.test]
./arrayOperations_unit_test.go:152:39: interface contains type constraints
./arrayOperations_unit_test.go:152:39: intOrString does not satisfy intOrString
./arrayOperations_unit_test.go:155:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:156:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:157:6: incompatible type: cannot use []int{} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:159:13: incompatible type: cannot use []int{} (value of type []int) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:163:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:164:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:165:6: incompatible type: cannot use []string{} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:167:13: incompatible type: cannot use []string{} (value of type []string) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:152:39: too many errors
FAIL    github.com/adam-hanna/arrayOperations/go2 [build failed]

答案1

得分: 25

如果你因为这个通用标题(双关语不是故意的)而遇到这个问题和答案,这里是关于联合类型的快速入门:

  1. 可以用于指定接口约束的类型集。泛型类型参数 T 将被限制为联合类型中的类型。
  2. 只能在接口约束中使用。如果一个接口包含一个联合类型(一个或多个项),那么它就是一个接口约束。
  3. 可以包含近似元素,使用 ~

例如:

type intOrString interface {
    int | string
}

func Foo[T intOrString](x T) {
    // x 可以是 int 或 string
}

现在来看看原始问题,提供更多细节:

不能将接口约束用作类型

通过包含类型集,intOrString 变成了一个接口约束,将其用作类型是明确不支持的。允许将约束作为普通接口类型

> 这是一个我们现在不建议的功能,但可以考虑在以后的语言版本中使用。

因此,首先要做的是将 intOrString 作为实际约束使用,在类型参数列表中使用它。下面我用 intOrString 替换了 comparable

type testDifferenceInput[T intOrString] [][]T
type testDifferenceOutput[T intOrString] []T
type testDifference[T intOrString] struct {
    input testDifferenceInput[T]
    output testDifferenceOutput[T]
}

这也意味着你不能使用约束来实例化一个具体类型作为你的测试切片:

// 错误:使用 intOrString 实例化一个参数化类型
[]testDifference[intOrString]

泛型容器不能容纳不同类型的项

你遇到的第二个问题是测试切片包含了两个不相关类型的结构体。一个是 testDifference[int],另一个是 testDifference[string]。即使 testDifference 类型本身被参数化为联合约束,它的具体实例化不是同一类型。详细信息请参阅这里

如果你需要一个包含不同类型的切片,你唯一的选择是 []interface{}(或 []any)...或者,你只需分开切片:

ttInts := []testDifference[int]{ testDifference[int]{...}, /* 等等 */ }
ttStrs := []testDifference[string]{ testDifference[string]{...}, /* 等等 */ }

联合约束允许的操作

只有类型集中的所有类型支持的操作。基于类型集的操作

> 规则是泛型函数可以以参数的类型为类型参数的方式使用值,只要该参数的约束的类型集中的每个成员都允许。

对于像 int | string 这样的约束,intstring 上允许的操作有哪些?简而言之:

  • 变量声明(var foo T
  • 转换和断言 T(x)x.(T),在适当的情况下
  • 比较(==!=
  • 排序(<<=>>=
  • + 运算符

因此,你可以有一个 intOrString 约束,但使用它的函数,包括你的 Difference 函数,受限于这些操作。例如:

type intOrString interface {
	int | string
}

func beforeIntOrString[T intOrString](a, b T) bool {
    return a < b
}

func sumIntOrString[T intOrString](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(beforeIntOrString("foo", "bar")) // false
    fmt.Println(beforeIntOrString(4, 5)) // true

    fmt.Println(sumIntOrString("foo", "bar")) // foobar
    fmt.Println(sumIntOrString(10, 5)) // 15
}
英文:

If you come across this Q&A because of its generic title (pun not intended), here's a quick primer about unions:

  1. Can be used to specify the type set of an interface constraint. A generic type parameter T will be restricted to the types in the union
  2. Can be used only in interface constraints. And if an interface contains a union (with one or more terms) then it is an interface constraint.
  3. Can include approximate elements with ~

For example:

type intOrString interface {
int | string
}
func Foo[T intOrString](x T) {
// x can be int or string
}

Now onto the OP's question, with some more details:

You can't use an interface constraint as a type

By including a type set, intOrString becomes an interface constraint, and using it as a type is explicitly not supported. Permitting constraints as ordinary interface types:

> This is a feature we are not suggesting now, but could consider for later versions of the language.

So the first thing to do is to use intOrString as an actual constraint, hence use it in a type parameter list. Below I replace comparable with intOrString:

type testDifferenceInput[T intOrString] [][]T
type testDifferenceOutput[T intOrString] []T
type testDifference[T intOrString] struct {
input testDifferenceInput[T]
output testDifferenceOutput[T]
}

This also means you can't use the constraint to instantiate a concrete type as your test slice:

// bad: using intOrString to instantiate a parametrized type
[]testDifference[intOrString]

A generic container can't hold items of different types

The second problem you have is that the test slice contains two structs of unrelated types. One is testDifference[int] and one is testDifference[string]. Even though the type testDifference itself is parametrized with a union constraint, its concrete instantiations are not the same type. See also this for further details.

If you need a slice holding different types, your only option is []interface{} (or []any) ...or, you just separate the slices:

ttInts := []testDifference[int]{ testDifference[int]{...}, /* etc. */ }
ttStrs := []testDifference[string]{ testDifference[string]{...}, /* etc. */ }

Allowed operations on union constraints

Only the operations supported by all types in the type set. Operations based on type sets:

> The rule is that a generic function may use a value whose type is a type parameter in any way that is permitted by every member of the type set of the parameter‘s constraint.

In case of a constraint like int | string, what are the operations permitted on either int or string? In short:

  • var declaration (var foo T)
  • conversions and assertions T(x) and x.(T), when appropriate
  • comparison (==, !=)
  • ordering (&lt;, &lt;=, &gt; and &gt;=)
  • the + operator

So you can have an intOrString constraint, but the functions that make use of it, including your func Difference, are limited to those operations. For example:

type intOrString interface {
int | string
}
func beforeIntOrString[T intOrString](a, b T) bool {
return a &lt; b
}
func sumIntOrString[T intOrString](a, b T) T {
return a + b
}
func main() {
fmt.Println(beforeIntOrString(&quot;foo&quot;, &quot;bar&quot;)) // false
fmt.Println(beforeIntOrString(4, 5)) // true
fmt.Println(sumIntOrString(&quot;foo&quot;, &quot;bar&quot;)) // foobar
fmt.Println(sumIntOrString(10, 5)) // 15
}

答案2

得分: 1

我已经通过 gotip 对你的代码进行了处理,它使用了更先进的实现方案,并且没有对代码的那部分进行投诉,所以我会认为问题出在 go2go 的初始实现上。

请注意,你的实现方式是不起作用的,因为你绝对可以在类型断言表达式中使用参数化接口,但是你不能像在 testDifference[intOrString] 中那样使用带有类型列表的接口。

英文:

I've passed your code through gotip that uses a more evolved implementation of the proposal and it does not complain about that part of the code, so I would assume that the problem is with the go2go initial implementation.

Please note that your implementation will not work since you can definitely use parametric interfaces in type assertion expressions, but you can't use interfaces with type lists as you are doing in testDifference[intOrString]

huangapple
  • 本文由 发表于 2021年10月30日 01:06:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/69772512.html
匿名

发表评论

匿名网友

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

确定