一句话总结
泛型(Generics)让一份代码适配多种类型,消除重复.
Rust 泛型语法与 Go 1.18+ 相似,但底层用单态化(monomorphization)--为每个具体类型生成专属代码,因此零运行时开销.
无约束的泛型类型能力很弱,真正的威力要等下一篇的特征约束来释放.
为什么需要泛型
假设要写"取序列中第一个元素"的函数.没有泛型,得为每种类型各写一遍:
|
逻辑相同,只是类型不同--这正是泛型要消除的重复,和 Go 1.18 之前的处境一样(要么复制粘贴,要么用 interface{} 牺牲类型安全).泛型给出第三条路:一份代码,编译期保持类型安全.
泛型函数
用尖括号声明类型参数 <T>,之后 T 就是一个类型占位符:
|
Rust 的 fn first<T>(list: &[T]) 几乎等同于 Go 的 func First[T any](list []T) T.
命名习惯一致(T 表示 type),思路也一致:把类型抽出来当参数.
无约束的 T 能力极弱
first 能编译,是因为它只做了"取引用"这种对任意类型都成立的操作.一旦想 a > b 比较,a + b 相加,编译器就会拒绝--它无法保证任意 T 都支持这些操作.
解决办法是特征约束(下一篇核心).这是 Rust 泛型与 Go 泛型共同的关键认知:泛型类型只能做"约束允许"的事.
泛型结构体与枚举
结构体,枚举也能泛型化.早就用过的 Option<T>,Vec<T> 就是泛型枚举/结构体:
|
在 impl 中使用泛型
给泛型结构体写方法,impl 后也要声明类型参数:
|
impl<T> 里的 T 是声明,Point<T> 里的 T 是使用.还能只为特定具体类型实现方法:
|
这种"只给某具体类型加方法"的能力,Go 泛型目前做不到,是 Rust 较灵活的一点.
单态化:零成本的秘密
这是 Rust 泛型与 Go 泛型在实现机制上的关键分野.编译 first<T> 时,Rust 会针对每个实际用到的类型,生成一份独立的具体代码:
|
结果:运行时没有任何泛型开销,等同于手写了多个专门版本.代价是编译产物可能变大,编译变慢.
单态化像"提前为每位客人定制专属餐具":用餐时(运行时)拿起就用,毫无适配成本;代价是后厨(编译期)要多做几套.
Go 泛型实现更接近"通用餐具 + 少量运行时适配",取舍不同--Rust 把开销全压到编译期,追求极致运行时性能.
类型推断与 turbofish
多数时候编译器能自动推断类型参数.无法推断时,用 ::<Type> 语法(俗称 turbofish)显式指定:
|
第 09 篇里 collect 常需类型标注,正是这个原因--它能产出多种集合,编译器需要明确目标类型.
下一篇预告
本篇的泛型还很"弱",因为类型参数没有约束.一旦给 T 加上特征约束(如"T 必须可比较大小""T 必须可打印"),泛型才真正强大起来.这正是下一篇「特征 Trait(上)」的主题--Rust 的接口系统.
快速回顾
- 泛型作用:一份代码适配多类型,消除重复,保持编译期类型安全.
- 语法:函数
fn f<T>,结构体struct S<T>,方法impl<T> S<T>,与 Go 高度相似. - 无约束 T:只能做对任意类型都成立的操作;要比较,运算需特征约束(下一篇).
- 特化 impl:可只为某具体类型(如
Point<f64>)实现方法,Go 做不到. - 单态化:为每个具体类型生成专属代码,运行时零开销,代价是产物变大.
- turbofish:
parse::<i32>()在推断不足时显式指定类型参数.
动手练习
- 泛型函数:写一个泛型函数
fn last<T>(list: &[T]) -> &T返回切片最后一个元素. - 约束不足:尝试写
fn largest<T>(list: &[T]) -> &T并在体内比较大小,观察编译错误,记住它(下一篇解决). - 泛型结构体:定义泛型结构体
Wrapper<T>,并在impl中实现取出内部值的方法. - 特化 impl:给一个泛型结构体只为某具体类型(如
Wrapper<String>)实现一个额外方法. - turbofish 使用:用 turbofish 把
"3.14"解析为f64,再用类型标注的等价写法实现一次.