Rust 的类型系统是比较吸引人的,实际上也算是一种代数类型系统(Algebraic data type),数学上有严格定义。
为什么有类型
计算机其实根本不关心类型。无论软件多么复杂,加上多少中间层,最终还是逃脱不了0和1。机器语言,是完全不关心类型的。我们也都清楚,这非常低效、冗长,特别容易出错。到了1950的时候,人们开始提出一些机器语言的助记符号,就有了现在仍然有用的汇编语言。汇编语言是一比一的翻译成机器语言的,没有编译的环境。
接下来是高级语言时代了,C语言等于在汇编语言上再加了一层抽象,C代码先编译成汇编语言,汇编语言再翻译成机器语言。我们都知道C语言是有类型的。类型和类型系统实际上提供了一种语义,指定什么是我想要的,什么是我不想要的,用来约束人类语言的模棱两可。
类型系统则是一套类型交互的规则。规则指明了哪些行为是正确的。Rust 的类型系统很大程度上受到了函数式编程的影响,比如 Ocaml 和 Haskell。比如,traits 就明显是 Haskell 的样子。
泛型
泛型提供了一种更高级的抽象。有的算法适用于多种类型,比如最简单的求和函数,这个算法就不关心是整数还是浮点数。如果在不支持的泛型的语言中,可能就需要把代码拷贝一遍,把参数作出相应的更改。然而,这样代码的维护起来的困难度就变大了。
泛型编程只适用在静态类型的编程语言中,最早出现在函数式编程语言 ML 中。动态语言,比如 Python,是不关心类型的,天生就带着泛型的气质,自然没有泛型的概念。静态语言有了泛型后,就可以用一个占位符比如 T
、K
、V
去代替具体类型声明,告诉编译器当初始化实例的时候再去填充具体的类型。
泛型函数
写一个简单的泛型函数,
// generic_function.rs
fn give_me<T>(value: T) {
let _ = value;
}
fn main() {
let a = "generics";
let b = 1024;
give_me(a);
give_me(b);
}
在编译的时候,编译器会生成两个版本的 give_me
,一个接受字符串类型,一个接受数字。可以通过 nm
命令来确定一下。
nm generic_function | grep "give"
nm 是 GNU 二进制工具包里的一个命令。通过把二进制文件传给 nm
然后使用 grep 过滤前缀为 give 的函数。
可以发现,泛型提供了一种简单的粗暴的多态。编译代码后自动生成多个版本的函数,这也带来了一个问题,由于代码的重复,最终可执行文件的大小会增加。不过大多数时候,使用泛型实现多态性是首选的方式,因为运行的时候没有开销。
如果能用泛型解决的问题,应该尽可能使用泛型。只有泛型不能满足要求,例如需要在某个集合容器里存取一组类型,才应该考虑用 trait。
泛型结构体和泛型枚举
和泛型函数很类似,没什么太大的差别,语法上没什么难以理解的:
struct GenericsStruct<T>(T);
struct Container<T> {
item: T;
}
enum Transmission<T> {
Signal(T),
NoSignal
}
fn main() {
// ...
}
泛型的 impl
同样可以使用 impl
来为泛型实现相应的方法。
struct Container<T> {
item: T
}
impl<T> Container<T> {
fn new(item: T) -> Self {
Container {
item }
}
}
这个方法适用于所有类型,但是有时候也可以实现特定类型的方法:
impl Container<u32> {
fn sum(item: u32) -> Self {
Container {
item }
}
}
因为这个方法只针对 u32
,所以不需要写 impl<T>
,只需要写 impl
就可以了。
使用泛型
要使用泛型,需要在编译时告诉编译器具体类型是什么,编译器才可以把 T
这种占位符替换成对应的类型。如果没有指明类型,代码是无法正确编译的:
// creating_generic_vec.rs
fn main() {
let a = Vec::new();
}
有几种方法可以告知编译器具体要什么类型:
// creating_generic_vec.rs
fn main() {
// providing a type
let v1: Vec<u8> = Vec::new();
// or calling method
let mut v2 = Vec::new();
v2.push(2); // v2 is new Vec<i32>
// or using turbofish
let v3 = Vec::<u8>::new(); // not so readable
}
编程中有一个很常见的操作,把数字字符串解析成数字。Rust 提供了一个泛型的 parse
方法,通过指定类型,如i32
、f32
、usize
,转换成不同的类型。
// using_generic.rs
use std::