why do we need generics?

Go 社区之父早期提到过 less is more 的哲学,可惜社区里有不少人被带偏了。

每次 Go 增加语法上的新特性、新功能,就会有原教旨主义者跳出来扔出一句 less is more(或者也可能是大道至简),扬长而去,留下风中凌乱的你。

即使到了官方已经确定要增加泛型功能的 2020 年,依然有人煞有介事地写文章说为什么 go doesn't need generics,作为理智的 Gopher,最好不要对别人的结论尽信,至少要看看其它语言社区怎么看待这件事情。

Java 社区是怎么理解泛型的必要性的呢?

简而言之,泛型使类型(类和接口)能够在定义类、接口和方法时成为参数。就像我们更熟悉的在方法声明中使用的形式参数一样,类型参数为你提供了一种用不同的输入重复使用相同代码的方法。不同的是,形式参数的输入是值,而类型参数的输入是类型。

与非泛型代码相比,使用泛型的代码有很多好处。

  • 在编译时进行强类型检查。
    Java 编译器对泛型代码进行强类型检查,如果代码违反类型安全就会报错。编译时的错误比运行时的错误更易修复。

  • 消除类型转换。
    下面这段代码片段在没有泛型时,需要类型转换:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

用泛型重写,代码不再需要进行类型转换:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast
  • 程序员可以编写泛型算法
    使用泛型可以实现在不同类型上都可以工作的泛型算法的同时,保证类型安全性。

Gopher 可能对“类型安全”不太熟悉,举个例子,我们可以用 gods 库来实现下面的数据结构。

package main

import (
	"fmt"

	sll "github.com/emirpasic/gods/lists/singlylinkedlist"
)

func main() {
	list := sll.New()
	list.Add("a")                     // ["a"]
}

我们的本意是实现一个 string 的单链表,但是通用的数据结构库没有办法阻止用户向该 list 内插入非 string 类型的值,比如用户可以这样:

	list := sll.New()
	list.Add("a")                     // ["a"]
    list.Add(2)                       // ["a", 2]

这显然不是我们想要的结果。

可见泛型最常见的场景是在类型安全的前提下实现算法流程,对于 Go 来说,我们使用的数据结构和算法来源有两个地方:container 标准库、第三方数据结构库,如 gods

和我们前面举的例子一样,标准库的通用 container 的大多接口也是接收空 interface{},或返回空 interface{}:

package main

import (
	"container/list"
)

func main() {
	l := list.New()
	l.PushBack(4)
	l.PushFront("bad value")
}

做不到类型安全的话,那么用户代码就可能在运行期间发生断言产生的 panic/error。除了容器的功能容易被破坏,类似下面的 bug 也挺容易出现的:

package main

import "fmt"

type mystring string

func main() {
	var a interface{} = "abc"
	var b interface{} = mystring("abc")
	fmt.Println(a == b)
}

社区的其它尝试

社区曾经有一些靠代码生成实现的泛型库,如genny,其本质是使用文本替换来实现多种类型的代码生成。

genny 使用也比较简单,比如 example 里的例子:

package queue

import "github.com/cheekybits/genny/generic"

// NOTE: this is how easy it is to define a generic type
type Something generic.Type

// SomethingQueue is a queue of Somethings.
type SomethingQueue struct {
  items []Something
}

func NewSomethingQueue() *SomethingQueue {
  return &SomethingQueue{items: make([]Something, 0)}
}
func (q *SomethingQueue) Push(item Something) {
  q.items = append(q.items, item)
}
func (q *SomethingQueue) Pop() Something {
  item := q.items[0]
  q.items = q.items[1:]
  return item
}

cat source.go | genny gen "Something=string"

// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny

package queue

// StringQueue is a queue of Strings.
type StringQueue struct {
  items []string
}

func NewStringQueue() *StringQueue {
  return &StringQueue{items: make([]string, 0)}
}
func (q *StringQueue) Push(item string) {
  q.items = append(q.items, item)
}
func (q *StringQueue) Pop() string {
  item := q.items[0]
  q.items = q.items[1:]
  return item
}

想实现多种类型的结构就在生成代码时传入多种类型就可以了。

这种做法和人们调侃 Go 泛型时使用的 gif 本质上也没什么区别。

语言的原生支持能让我们省事,并且也能在实现上更加严谨。

在 《Rise and Fall of Software Recipes》一书中,有这么一个故事:

Among the recent projects failing because (or despite) of strong processes, Obamacare is a telling example. It involves 50 contractors, has cost fortunes, was delivered late and crippled with bugs. It was developed using a typical waterfall process, and if only because of that, the Agile community started howling, claiming that they would have made the project a success[Healthcare.gov failure].

And when an Agile project fails like Universal Credit in Great-Britain[UniversalCredit] [NAO2013], even when the full report states that the lack of detailed blueprint – typical of Agile methodologies – was one of the factors that caused the failure, common Agile wisdom says it is because it was not applied properly, or should I say, not Agile enough.

简而言之,就是敏捷大师们其实非常双标,他们给出的方法论也不一定靠谱,反正成功了就是大师方法得当,失败了就是我们执行不力没有学到精髓。正着说反着说都有道理。

再看看现在的 Go 社区,buzzwords 也很多,如果一个特性大师不想做,那就是 less is more。如果一个特性大师想做,那就是 orthogonal,非常客观。

对于不想迷信大师的 Gopher 来说,多听听批评意见没坏处:go is not good