英文:
What's the difference between a generic slice argument and an argument constrained to slice types?
问题
考虑一下实验性包slices
。由于该包是实验性的,所以我理解签名可能会发生变化;我使用它来说明问题。
考虑一下这个包中两个函数的签名,slices.Contains
和slices.Grow
:
-
func Contains[E comparable](s []E, v E) bool
-
func Grow[S ~[]E, E any](s S, n int) S
Contains
函数的第一个参数的类型是[]E
(E
类型的切片),其中E
受到comparable
(可比较的类型)的约束。
而Grow
函数的第一个参数的类型是S
(只是S
),其中S
受到~[]E
(底层类型为E
的切片类型)的约束。
然而,看起来在具有这种类型参数的函数内部允许的操作之间没有任何实际区别。如果我们声明一些具有相同类型参数的虚假函数,我们可以看到两者都可以编译通过:
如预期的那样,在这两个函数中,我们可以使用len
/cap
、append
、range
、make
进行分配,以及使用[
]
进行索引。
即使在所有情况下,reflect.TypeOf(s).Kind()
也会给出reflect.Slice
。
这些函数还可以使用不同的类型进行测试,而且都可以编译通过:
唯一实际的区别在于,在slices.Grow
中,参数s S
不是切片。它只是受到切片类型的约束。事实上,当参数是type MyUint64Slice []uint64
的实例时,reflect.TypeOf(s)
会给出不同的输出:
- 使用参数
s []E
的Contains
函数会给出reflect.TypeOf(s) -> []uint64
- 使用参数
s S
的Grow
函数会给出reflect.TypeOf(s) -> main.MyUint64Slice
然而,对我来说,这两者之间的实际区别并不明显。
在实践中,这两个声明是否等效?如果不是,我应该在什么情况下选择其中之一?
英文:
Consider the experimental package slices
. The package is experimental, so I understand the signatures may change; I'm using it to illustrate the issue.
Consider the signatures of two functions from this package, slices.Contains
and slices.Grow
:
-
func Contains[E comparable](s []E, v E) bool
-
func Grow[S ~[]E, E any](s S, n int) S
The first argument to Contains
has type []E
(slice of E
s) with E
constrained by comparable
(types that are comparable).
The first argument to Grow
instead has type S
(just S
), with S
constrained by ~[]E
(types whose underlying type is a slice of E
)
However it looks like there isn't any practical difference between what operations are allowed inside functions with such type params. If we declare some fake funcs with the same type parameters, we can see that both compile just fine:
As expected, in both functions we can len
/cap
, append
, range
, allocate with make
, and index with [
]
.
func fakeContains[E comparable](s []E, v E) {
fmt.Println(len(s), cap(s))
var e E
fmt.Println(append(s, e))
fmt.Println(make([]E, 4))
for _, x := range s {
fmt.Println(x)
}
fmt.Println(s[0])
fmt.Println(reflect.TypeOf(s).Kind())
}
func fakeGrow[S ~[]E, E any](s S, n int) {
fmt.Println(len(s), cap(s))
var e E
fmt.Println(append(s, e))
fmt.Println(make(S, 4))
for _, x := range s {
fmt.Println(x)
}
fmt.Println(s[0])
fmt.Println(reflect.TypeOf(s).Kind())
}
Even reflect.TypeOf(s).Kind()
gives reflect.Slice
in all cases.
The functions can also be tested with different types, and all compile:
// compiles just fine
func main() {
type MyUint64 uint64
type MyUint64Slice []uint64
foo := []uint64{0, 1, 2}
fakeContains(foo, 0)
fakeGrow(foo, 5)
bar := []MyUint64{3, 4, 5}
fakeContains(bar, 0)
fakeGrow(bar, 5)
baz := MyUint64Slice{6, 7, 8}
fakeContains(baz, 0)
fakeGrow(baz, 5)
}
The only actual difference in my understanding is that in slices.Grow
the argument s S
is not a slice. It's just constrained to slice types. And as a matter of fact reflect.TypeOf(s)
gives a different output when the arg is an instance of type MyUint64Slice []uint64
:
Contains
with args []E
givesreflect.TypeOf(s) -> []uint64
Grow
with args S
givesreflect.TypeOf(s) -> main.MyUint64Slice
However it's not immediately apparent to me what's the practical difference between the two.
Playground with the code: https://gotipplay.golang.org/p/zg2dGtSJwuI
Question
Are these two declarations equivalent in practice? If not, when should I choose one over the other?
答案1
得分: 6
如果你需要返回与参数相同(可能具有命名)类型的切片,那么这是很重要的。
如果你不需要返回一个切片(只需要其他一些信息,比如一个bool
来报告值是否包含在内),你不需要使用一个类型参数来约束切片本身,你可以只使用一个元素的类型参数。
如果你需要返回与输入相同类型的切片,你必须使用一个类型参数来约束切片本身(例如~[]E
)。
为了演示,让我们看看Grow()
的这两个实现:
func Grow[S ~[]E, E any](s S, n int) S {
return append(s, make(S, n)...)[:len(s)]
}
func Grow2[E any](s []E, n int) []E {
return append(s, make([]E, n)...)[:len(s)]
}
如果你传递一个自定义类型的切片作为其底层类型,Grow()
可以返回相同类型的值。Grow2()
则不行:它只能返回一个未命名切片类型的值:[]E
。
下面是演示的代码:
x := []int{1}
x2 := Grow(x, 10)
fmt.Printf("x2 %T len=%d cap=%d\n", x2, len(x2), cap(x2))
x3 := Grow2(x, 10)
fmt.Printf("x3 %T len=%d cap=%d\n", x3, len(x3), cap(x3))
type ints []int
y := ints{1}
y2 := Grow(y, 10)
fmt.Printf("y2 %T len=%d cap=%d\n", y2, len(y2), cap(y2))
y3 := Grow2(y, 10)
fmt.Printf("y3 %T len=%d cap=%d\n", y3, len(y3), cap(y3))
输出结果(在Go Playground上尝试):
x2 []int len=1 cap=12
x3 []int len=1 cap=12
y2 main.ints len=1 cap=12
y3 []int len=1 cap=12
正如你所看到的,Grow2(y, 10)
接收到的是类型为main.ints
的值,但它返回的是类型为[]int
的值。这不是我们希望得到的结果。
英文:
It matters if you have to return a slice of the same (possibly named) type as the argument.
If you do not have to return a slice (just some other info e.g. a bool
to report if the value is contained), you do not need to use a type parameter that itself constraints to a slice, you may use a type parameter for the element only.
If you have to return a slice of the same type as the input, you must use a type parameter that itself constraints to a slice (e.g. ~[]E
).
To demonstrate, let's see these 2 implementations of Grow()
:
func Grow[S ~[]E, E any](s S, n int) S {
return append(s, make(S, n)...)[:len(s)]
}
func Grow2[E any](s []E, n int) []E {
return append(s, make([]E, n)...)[:len(s)]
}
If you pass a slice of a custom type having a slice as its underlying type, Grow()
can return a value of the same type. Grow2()
cannot: it can only return a value of an unnamed slice type: []E
.
And the demonstration:
x := []int{1}
x2 := Grow(x, 10)
fmt.Printf("x2 %T len=%d cap=%d\n", x2, len(x2), cap(x2))
x3 := Grow2(x, 10)
fmt.Printf("x3 %T len=%d cap=%d\n", x3, len(x3), cap(x3))
type ints []int
y := ints{1}
y2 := Grow(y, 10)
fmt.Printf("y2 %T len=%d cap=%d\n", y2, len(y2), cap(y2))
y3 := Grow2(y, 10)
fmt.Printf("y3 %T len=%d cap=%d\n", y3, len(y3), cap(y3))
Output (try it on the Go Playground):
x2 []int len=1 cap=12
x3 []int len=1 cap=12
y2 main.ints len=1 cap=12
y3 []int len=1 cap=12
As you can see Grow2(y, 10)
receives a value of type main.ints
and yet it returns a value of type []int
. This is not what we would want from it.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论