泛型(Generics) 这一概念比较古老,从 80 年代末的 Ada 开始一直到现在成为了静态类型语言的标配,包括 Haskell、C++、Java、Go、Rust。而这系列文章主要描述 Rust 中的泛型,对其他语言并不会过多涉及。
泛型是一种多态
泛型是一种多态(Polymorphism), 具体来说是参数多态的一种实现。那么多态又是什么呢?对于它的解释经常存在歧义,原因是多态有很多不同的类型。
-
参数多态(Parametric Polymorphism): 比如 Rust 的泛型就是一种参数多态,在编译时就会确定具体的类型,采用单态化(Monomorphization)的方式生成代码,运行时不会存在任何的多态行为,因此也不存在任何的性能损失。Java 的实现方式不一样,采用的是编译时类型擦除,这就导致了运行时是无法获取到完整的类型信息的,只能通过反射获取部分信息。
-
子类型多态(Subtype Polymorphism): 一般是运行时多态,比如在 OOP 中谈论到多态,父类型的引用可以指向子类型的实例。可能会采用虚表(vtable)的方式来实现,每一个类都有一张方法表,调用时通过这张表来查找具体的函数地址。存在运行时开销。在 Rust 中,这类多态是通过 Trait Object 来实现的。
-
重载多态(Ad-hoc Polymorphism): 比如方法重载、操作符重载都属于这一类,根据方法或者操作符不同实现多份代码。这类多态表现为函数签名不同或者参数类型不同。
不同语言中对泛型的不同实现
需要额外说明的是,Java 和 Rust 都支持参数多态和子类型多态。比如 Rust 的 Trait Object 就是运行时多态。两种语言的示例如下:
java
List<String> list = new ArrayList(); // 参数多态
Animal animal = new Dog(); // 子类型多态
我们再来看 Rust 中的多态:
rust
fn print<T: Debug>(x: T) {} // 参数多态
fn print(x: &dyn Debug) {} // 子类型多态
在动态类型的语言中,比如 Python、Ruby、PHP、JavaScript 是通过鸭子类型(Duck Typing)来实现多态的。我们来看一个 Python 的例子:
python
def print_length(obj):
print(len(obj))
print_length([1, 2, 3]) # 传入数组可以
print_length("hello") # 传入字符串也可以
print_length(100) # 运行时报错,因为数值类型没有 len()
动态类型的语言中,非常灵活,不需要特别声明类型。但是,鸭子类型也存在一些问题,比如在编译时无法检查类型,运行时会报错, 这种类型的错误需要通过更多的单元测试、边界测试才能发现。这种灵活性,对开发者的要求也会更高,当然自由度更高。
现代的一些编程语言,也从弱类型语言中吸取其灵活的特性,比如鸭子类型。在 GO 语言中的接口,你也不需要声明实现了这个接口,只要实现了接口所需的方法后就可以作为出参数传入。Rust 中为了提升灵活性,引入了 trait object,这些都是"动态语言"的优点的体现,灵活性。但是会存在运行时开销。
下面这个例子演示了 Go 语言中静态的鸭子类型的案例, 兼顾了性能和灵活性:
go
type Speaker interface {
Speak()
}
type Dog struct{}
func (d Dog) Speak() {
fmt.Println("汪汪!")
}
func main() {
dog := Dog{}
// 由于 Dog 实现了 Speak 方法
// 它们都可以作为 Speaker 类型参数传递
makeSound(dog)
}
其实静态类型的语言和动态类型的语言现在是相互学习,静态类型的学习动态性,而动态类型的语言则学习静态型。比如 PHP 、Python 在完善类型系统,而 Go、Rust 、C++ 则通过动态分发来提升灵活性。
总结
在对比了一些其他语言之后,我们回过头来看 Rust 中的泛型,本质就是一种参数多态,将类型作为一种参数传入。因为是编译时多态,相比动态语言更加的安全和更高的性能,也兼顾了灵活性。就算相比其他静态类型的语言,Rust 的泛型有自己的很多特色,后续的文章详细分析。