英文:
Why does maps.Keys() in Go specify the map type as M?
问题
我有一个函数用于获取映射中的键(实际上有几个版本,用于不同的类型),我在 Go 1.18 中更新了该函数以使用泛型。然后我发现实验性库被扩展以包括该功能,虽然我的实现几乎相同,但函数声明有一些差异,我想更好地理解这些差异。
这是我的原始泛型版本(我重命名了变量以匹配标准库,以更好地突出实际上的不同之处):
func mapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
这是标准库版本:
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
你可以看到,主要区别是额外的 M ~map[K]V
类型参数,我省略了它,并直接使用了 map[K]V
作为函数的参数类型。我的函数可以正常工作,那么为什么我需要额外添加第三个参数化类型呢?
当我写问题时,我以为我已经找到了答案:为了能够在实际上是映射的类型上调用该函数,但并没有直接声明为映射类型,比如可能是这个 DataCache
类型:
type DataCache map[string]DataObject
我的想法是这可能需要 ~map
表示法,并且 ~
只能在类型约束中使用,而不能在实际类型中使用。只有一个问题:我的版本在这样的映射类型上可以正常工作。所以我不知道它有什么用处。
英文:
I had a function implemented to obtain the keys in a map (a few versions actually, for different types), which I updated to use generics in Go 1.18. Then I found out that the experimental library was extended to include that functionality, and while my implementation was nearly identical, the function declaration has some differences that I would like to better understand.
Here is my original generic version (I renamed the variables to match the standard library, to better highlight was is actually different):
func mapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
And here is the standard-library version:
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
As you can see, the main difference is the extra M ~map[K]V
type parameter, which I omitted and directly used map[K]V
for the function's argument type. My function works, so why would I need to go through the extra trouble of adding a third parameterized type?
As I was writing my question, I thought I had figured out the answer: to be able to invoke the function on types that are really maps under the covers, but are not directly declared as such, like maybe on this DataCache
type:
type DataCache map[string]DataObject
My thinking was that this probably required the ~map
notation, and the ~
can only be used in a type constraint, not in an actual type. Only problem with this theory: my version works fine on such map types. So I am at a loss as to what it's useful for.
答案1
得分: 5
tl;dr 这在非常罕见的情况下是相关的,即当你需要声明一个函数类型的变量(而不调用它),并且你使用另一个包中使用未导出类型的命名映射类型来实例化该函数时。
使用命名类型参数在函数签名中主要是在需要接受和返回已定义类型时才相关,正如你正确猜测的那样,并且正如 @icza 在这里回答的关于 x/exp/slices
包的问题。
你提到的“波浪线类型”只能在接口约束中使用的观点也是正确的。
现在,x/exp/maps
包中几乎所有的函数实际上都不返回命名类型 M
。唯一一个实际返回命名类型 M
的函数是 maps.Clone
,其签名为:
func Clone[M ~map[K]V, K comparable, V any](m M) M
然而,对于已定义类型,即使在签名中没有近似约束 ~map[K]V
,也可以正常工作,这要归功于 类型统一。根据规范:
> [...], 因为定义的类型 D
和类型字面量 L
永远不等效,统一将 D 的底层类型与 L 进行比较。
以下是一个代码示例:
func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
type Dictionary map[string]int
func main() {
m := Dictionary{"foo": 1, "bar": 2}
k := Keys(m)
fmt.Println(k) // 它可以正常工作
}
Playground: https://go.dev/play/p/hzb2TflybZ9
当需要传递函数的实例化值时,额外的命名类型参数 M ~map[K]V
是相关的:
func main() {
// 函数类型的变量!
fn := Keys[Dictionary]
m := Dictionary{"foo": 1, "bar": 2}
fmt.Println(fn(m))
}
Playground: https://go.dev/play/p/hks_8bnhgsf
如果没有 M ~map[K]V
类型参数,将无法使用已定义类型实例化这样的函数值。当然,你可以像这样分别使用 K
和 V
实例化函数:
fn := Keys[string, int]
但是,当定义的映射类型属于不同的包并引用未导出类型时,这种方法就不可行了:
package foo
type someStruct struct{ val int }
type Dictionary map[string]someStruct
和:
package main
func main() {
// 无法编译通过
// fn := Keys[string, foo.someStruct]
// 这样可以
fn := maps.Keys[foo.Dictionary]
}
不过,这似乎是一个相当奇特的用例。
你可以在这里查看最终的 playground:https://go.dev/play/p/B-_RBSqVqUD
但请记住,x/exp/maps
是一个实验性的包,因此在未来的 Go 版本中,签名可能会发生变化,或者当这些函数被提升到标准库时会发生变化。
英文:
tl;dr it is relevant in the very uncommon case where you need to declare a variable of function type (without calling it), and you instantiate that function with a named map type from another package that uses unexported types in its definition.
<hr>
Using a named type parameter in function signatures is mostly relevant when you need to accept and return defined types, as you correctly guessed, and as @icza answered here with respect to the x/exp/slices
package.
Your remark that "tilde types" can only be used in interface constraints is also correct.
Now, almost all functions in the x/exp/maps
package do not actually return the named type M
. The only one that actually does is maps.Clone
with signature:
func Clone[M ~map[K]V, K comparable, V any](m M) M
However declaring the signature without the approximate constraint ~map[K]V
still works for defined types thanks to type unification. From the specs:
> [...], because a defined type D
and a type literal L
are never equivalent, unification compares the underlying type of D with L instead
And a code example:
func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
type Dictionary map[string]int
func main() {
m := Dictionary{"foo": 1, "bar": 2}
k := Keys(m)
fmt.Println(k) // it just works
}
Playground: https://go.dev/play/p/hzb2TflybZ9
The case where the additional named type parameter M ~map[K]V
is relevant is when you need to pass around an instantiated value of the function:
func main() {
// variable of function type!
fn := Keys[Dictionary]
m := Dictionary{"foo": 1, "bar": 2}
fmt.Println(fn(m))
}
Playground: https://go.dev/play/p/hks_8bnhgsf
Without the M ~map[K]V
type parameter, it would not be possible to instantiate such a function value with defined types. Of course you could instantiate your function with K
and V
separately like
fn := Keys[string, int]
But this is not viable when the defined map type belongs to a different package and references unexported types:
package foo
type someStruct struct{ val int }
type Dictionary map[string]someStruct
and:
package main
func main() {
// does not compile
// fn := Keys[string, foo.someStruct]
// this does
fn := maps.Keys[foo.Dictionary]
}
Though, this seems a rather esoteric use case.
You can see the final playground here: https://go.dev/play/p/B-_RBSqVqUD
However keep in mind that x/exp/maps
is an experimental package, so the signatures may be changed with future Go releases, and/or when these functions get promoted into the standard library.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论