为什么在Go语言中,maps.Keys()指定地图类型为M?

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

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 类型参数,将无法使用已定义类型实例化这样的函数值。当然,你可以像这样分别使用 KV 实例化函数:

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{&quot;foo&quot;: 1, &quot;bar&quot;: 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{&quot;foo&quot;: 1, &quot;bar&quot;: 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.

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

发表评论

匿名网友

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

确定