为什么Go中maps.Keys()指定map类型为M?

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
}

这里是 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
}

如您所见,主要区别在于额外的 M ~map[K]V 类型参数,我将其省略并直接使用 map[K]V 作为函数的参数类型。我的函数有效,那么为什么我需要经历添加第三个参数化类型的额外麻烦?

当我写我的问题时,我想我已经找到了答案:能够在真正映射的类型上调用函数,但没有直接声明为映射,就像可能在这个DataCache 类型:

type DataCache map[string]DataObject

我的想法是,这可能需要 ~map 符号,而 ~ 只能用于类型约束,不能用于实际类型。该理论的唯一问题是:我的版本在此类地图类型上运行良好。所以我不知道它有什么用。

tl;dr 它与 非常不常见的 情况相关,您需要声明一个函数类型的变量(而不调用它),然后使用另一个包中的命名映射类型实例化该函数,该包在其定义中使用未导出的类型。


当您需要接受 和 return 定义的类型时,在函数签名中使用命名类型参数最为相关,正如您正确猜测的那样,正如@icza 回答的那样 关于 x/exp/slices 包。

您关于“代字号类型”只能用于接口约束的说法也是正确的。

现在,x/exp/maps 包中的几乎所有函数实际上 return 命名类型 M。唯一真正做到的是 maps.Clone 签名:

func Clone[M ~map[K]V, K comparable, V any](m M) M

然而,声明签名 没有 近似约束 ~map[K]V 仍然适用于定义的类型,这要归功于 type unification。来自规格:

[...], because a defined type D and a type literal L are never equivalent, unification compares the underlying type of D with L instead

还有一个代码示例:

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
}

游乐场:https://go.dev/play/p/hzb2TflybZ9

与附加命名类型参数 M ~map[K]V 相关的情况是当您需要传递 函数的实例化值时 :

func main() {
    // variable of function type!
    fn := Keys[Dictionary]
    
    m := Dictionary{"foo": 1, "bar": 2}
    fmt.Println(fn(m))
}

游乐场: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() {
    // does not compile
    // fn := Keys[string, foo.someStruct]

    // this does
    fn := maps.Keys[foo.Dictionary]
}

不过,这似乎是一个相当通俗易懂的用例。

你可以在这里看到最后的游乐场:https://go.dev/play/p/B-_RBSqVqUD

但是请记住,x/exp/maps 是一个实验包,因此签名可能会随着未来的 Go 版本而改变,and/or 当这些函数被提升到标准库中时。