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

Mar*_*ier 7 generics go

我实现了一个函数来获取映射中的键(实际上有几个版本,针对不同的类型),我将其更新为在 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
}
Run Code Online (Sandbox Code Playgroud)

这是实验库版本

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
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,主要区别是额外的M ~map[K]V类型参数,我省略了它并直接用于map[K]V函数的参数类型。我的函数可以工作,那么为什么我需要经历添加第三个参数化类型的额外麻烦呢?

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

type DataCache map[string]DataObject
Run Code Online (Sandbox Code Playgroud)

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

bla*_*een 6

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


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

您关于“波形符类型”只能在接口约束中使用的说法也是正确的。

现在,包中的几乎所有函数x/exp/maps实际上并不返回指定的 type M。唯一真正起作用的是maps.Clone签名:

func Clone[M ~map[K]V, K comparable, V any](m M) M
Run Code Online (Sandbox Code Playgroud)

然而,由于类型统一,在没有近似约束的情况下声明签名~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) // it just works
}
Run Code Online (Sandbox Code Playgroud)

游乐场: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))
}
Run Code Online (Sandbox Code Playgroud)

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

如果没有M ~map[K]V类型参数,就不可能用定义的类型实例化这样的函数值。当然,您可以使用KV分别实例化您的函数,例如

fn := Keys[string, int]
Run Code Online (Sandbox Code Playgroud)

但是,当定义的映射类型属于不同的包并引用未导出的类型时,这是不可行的:

package foo 

type someStruct struct{ val int }

type Dictionary map[string]someStruct
Run Code Online (Sandbox Code Playgroud)

和:

package main

func main() {
    // does not compile
    // fn := Keys[string, foo.someStruct]

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

Run Code Online (Sandbox Code Playgroud)

不过,这似乎是一个相当深奥的用例。

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

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