V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
songtianyi
V2EX  ›  程序员

Go2 泛型设计草案更新

  •  
  •   songtianyi · 2020-12-09 18:50:44 +08:00 · 2421 次点击
    这是一个创建于 1225 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Go2 泛型设计草案更新

    作者: songtianyi create@2020/12/03

    前言

    18 年的时候,go team 发布了 Go2 的几个新特性的草案,其中包括呼声较高的泛型,当时写了一篇文章做了介绍。最近 Go team 对泛型的设计草案进行了较大的改动,有必要更新下这个改动并分享出来。

    contracts

    在 18 年释出的草案中,是使用 contract 来约束泛型的类型参数(type parameters)的,最新的草案放弃了这种做法, 用已有的概念 interface 代替。 在继续之前,先来熟悉 type parameter 这个概念:

    Generic code is code that is written using types that will be specified later. Each unspecified type is called a type parameter. When running the generic code, the type parameter will be set to a type argument.

    好,继续。回顾下 contract 形式的例子:

    contract stringer(T) {
    	T String() string
    }
    
    func Stringify(type T stringer)(s []T) (ret []string) {
    	for _, v := range s {
    		ret = append(ret, v.String()) // now valid
    	}
    	return ret
    }
    
    strSlice = []string{}
    
    

    上述代码约束了入参 s 的类型 T 必须是实现了 String 函数的类型

    interface

    那么改用 interface 之后怎么做?

    // Stringer is a type constraint that requires the type argument to have
    // a String method and permits the generic function to call String.
    // The String method should return a string representation of the value.
    type Stringer interface {
    	String() string
    }
    
    // Stringify calls the String method on each element of s,
    // and returns the results.
    func Stringify[T Stringer](s []T) (ret []string) {
    	for _, v := range s {
    		ret = append(ret, v.String())
    	}
    	return ret
    }
    
    

    上述代码,使用 Stringer interface 来约束入参 s 的类型 T 必须是实现了 String() string 函数的类型。 除了使用自定义的 interface 来约束之外,Go 内置了 any 来指明入参是可以为任意类型的, 当我们不需要约束的时候可以使用 any 来维持写法的一致性, any 相当于 interface{}

    // Print prints the elements of any slice.
    // Print has a type parameter T and has a single (non-type)
    // parameter s which is a slice of that type parameter.
    func Print[T any](s []T) {
        for _, v := range s {
    		fmt.Println(v)
    	}
    }
    

    interface 我们经常会用到,是一个已经非常熟悉的概念,而且使用 interface 可以避免不必要的重复定义的情况。以上面的 Stringer 为例,对 Stringify 函数,如果使用 contract 来进行约束,我们需要定义:

    // 约束
    contract stringer_c(T) {
    	T String() string
    }
    
    // Stringer 接口
    type Stringer interface {
    	String() string
    }
    
    // 入参 s 被约束为实现了 String() string 函数的类型
    func Stringify[T stringer_c](s []T) (ret []string) {
    	for _, v := range s {
    		ret = append(ret, v.String())
    	}
    	return ret
    }
    
    // 实现了 String() string 的结构体
    type IStringer struct {
    	v string
    }
    
    // String() string 实现
    func (i *IStringer) String() string {
    	return v
    }
    
    var i_stringer IStringer
    Stringfy(i_stringer) // 合法入参
    

    从上面的代码可以看出, stringer_c contract 其实和 Stringer interface 是重复的。 这和 Stringer interface 的定义其实是重复的。

    看到这里是不是觉得这个改动还是很棒的?相对 contract 来说,interface 更好理解,有时候也可以省掉重复的定义。 但是,interface 只能定义函数,因此,我们只能使用 interface 来约束 T 必须实现的函数,而不能约束 T 所能支持的运算。 使用 contract 来约束类型参数所支持的运算符的例子:

    // comparable contract
    contract ordered(t T) {
      t < t
    }
    func Smallest[T ordered](s []T) T {
    	r := s[0] // panic if slice is empty
    	for _, v := range s[1:] {
    		if v < r { // OK
    			r = v
    		}
    	}
    	return r
    }
    

    很方便。 但使用 interface 就没那么方便了:

    package constraints
    
    // Ordered is a type constraint that matches any ordered type.
    // An ordered type is one that supports the <, <=, >, and >= operators.
    type Ordered interface {
    	type int, int8, int16, int32, int64,
    		uint, uint8, uint16, uint32, uint64, uintptr,
    		float32, float64,
    		string
    }
    
    // Smallest returns the smallest element in a slice.
    // It panics if the slice is empty.
    func Smallest[T constraints.Ordered](s []T) T {
    	r := s[0] // panics if slice is empty
    	for _, v := range s[1:] {
    		if v < r {
    			r = v
    		}
    	}
    	return r
    }
    

    要写一大堆... 心里一阵 mmp... 先别慌!

    Ordered interface 里列出来的类型是 Ordered 约束可以接受的类型参数。由此看来,针对运算符的约束写起来变的更复杂了,幸运的是,go 会内置常用的约束, 不用我们自己来写. 而且,约束是可以组合的:

    // ComparableHasher is a type constraint that matches all
    // comparable types with a Hash method.
    type ComparableHasher interface {
    	comparable
    	Hash() uintptr
    }
    

    上述代码是一个约束,它约束类型参数必须是可比较的,而且实现了 Hash() uintptr 函数。

    // StringableSignedInteger is a type constraint that matches any
    // type that is both 1) defined as a signed integer type;
    // 2) has a String method.
    type StringableSignedInteger interface {
    	type int, int8, int16, int32, int64
    	String() string
    }
    

    类似地,可以将可接受的类型列表(type list)和函数约束放在一起。

    讲到这里,关于泛型改动的核心内容已经讲完了,更复杂的用法可以查看文档 go2draft-type-parameters.

    个人认为,这个改动是一个比较成功的改动,没有引入新的概念,通过内置一些约束,支持约束组合来方便开发者。

    17 条回复    2020-12-10 10:33:35 +08:00
    zxCoder
        1
    zxCoder  
       2020-12-09 18:54:10 +08:00
    加个泛型有这么复杂吗? 好奇怪
    wysnylc
        2
    wysnylc  
       2020-12-09 18:56:30 +08:00
    @zxCoder #1 有,因为最初版本的设计是不加泛型
    从 2010 年就有泛型的提案,直到现在也没转正所以你懂 google 那群人的意思了吧
    songtianyi
        3
    songtianyi  
    OP
       2020-12-09 18:58:40 +08:00
    内容更正:
    Stringfy(i_stringer) // 合法入参 ➡️ Stringfy([]IStringer{i_stringer}) // 合法入
    luob
        4
    luob  
       2020-12-09 19:03:36 +08:00 via iPhone   ❤️ 3
    “同意加泛型的点赞,不同意加泛型的点踩”
    “我同意加泛型,可我对每个泛型草案都点了踩”
    “那您快请加入到 go 核心团队来”
    12101111
        5
    12101111  
       2020-12-09 19:07:58 +08:00
    和 rust 的 trait 一样啊
    Jirajine
        6
    Jirajine  
       2020-12-09 19:09:21 +08:00 via Android
    >不能约束 T 所能支持的运算
    T 支持 xx 运算和实现 xx 方法本来就是一回事,为什么非要搞两个概念呢?
    lewis89
        7
    lewis89  
       2020-12-09 19:10:09 +08:00
    倒是给个泛型发版的时间啊,都这么久了 还没发布 2.0 的意思
    zxCoder
        8
    zxCoder  
       2020-12-09 19:10:36 +08:00
    @wysnylc 泛型这东西到底有啥坑吗? 按道理搞一个编程语言出来不应该是取其他语言之精华去其糟粕
    songtianyi
        9
    songtianyi  
    OP
       2020-12-09 19:17:01 +08:00
    @Jirajine 因为修改后的方案是 使用 已有的概念,interface 已有的概念里是没有这些东东的。
    ```
    type Ordered interface {
    type int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64, uintptr,
    float32, float64,
    string
    }
    ```
    所以以递进的形式来讲。事实上,像你说的,本来就是一回事。
    12101111
        10
    12101111  
       2020-12-09 19:19:02 +08:00
    我很好奇未来 go 的类型系统会不会进一步演进, 比如 generic associated types, higher-kinded types
    victor
        11
    victor  
       2020-12-09 19:19:12 +08:00
    希望 2030 年后再添加泛型
    songtianyi
        12
    songtianyi  
    OP
       2020-12-09 19:23:48 +08:00
    @12101111 我觉得不会,类型系统是一个语言的最主要的特征之一,改多了就不是 go 了。
    songtianyi
        13
    songtianyi  
    OP
       2020-12-09 19:30:21 +08:00
    @12101111 是挺像的。
    aloxaf
        14
    aloxaf  
       2020-12-09 19:36:37 +08:00
    @12101111
    你这个好奇也太离谱了点。你不如好奇美国什么时候会走社会主义道路,我觉得这都比 Go 加 HKT 要现实……
    zjsxwc
        15
    zjsxwc  
       2020-12-09 20:12:31 +08:00 via Android
    第二种方法,用 interface 更加容易理解,我站第二种。
    laball
        16
    laball  
       2020-12-10 09:31:56 +08:00
    感觉语言特性发展过程中,有一种弊病,就是别人已经用了这种方式,我就得不一样,不然显得是在抄作业。
    我就想说,还 TM 嫌括号不够多嘛,加个特性,加一组括号,这是要干啥。。。
    songtianyi
        17
    songtianyi  
    OP
       2020-12-10 10:33:35 +08:00
    @laball 看了样例代码,确实眼花缭乱的 😄.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5418 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 05:46 · PVG 13:46 · LAX 22:46 · JFK 01:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.