Rust 学习笔记 14:泛型 (Generics)

“Abstraction without overhead.”

泛型是静态类型语言提高代码复用率的关键。 在 Go 中,我们习惯了 interface{} (或 any) 带来的运行时动态检查,或者复制粘贴代码。 Rust 的泛型则不同,它在编译期通过单态化 (Monomorphization) 展开代码,运行时没有性能损耗。

1. 泛型函数

假设我们要写一个找最大值的函数,既能找 i32 也能找 char

1fn get_largest<T: PartialOrd + Copy>(list: &[T]) -> T {
2    let mut largest = list[0];
3    for &item in list {
4        if item > largest { // 需要 T 实现 PartialOrd
5            largest = item;
6        }
7    }
8    largest
9}

这里 T 是类型参数。<T: PartialOrd + Copy> 叫做 Trait Bound(类似 Java 的 T extends ... 或 Go 的 T interface ...)。它告诉编译器:这个 T 不是任意类型,它必须能比较大小,而且能被 Copy(为了简单起见,这里假设是栈上数据)。

2. 泛型结构体与枚举

我们之前用过的 Option<T>Result<T, E> 就是典型的泛型枚举。 自定义泛型结构体也很简单:

1struct Point<T> {
2    x: T,
3    y: T,
4}

注意:这里 xy 必须是同一种类型 T。 如果是 Point { x: 5, y: 4.0 },编译器会报错。如果想不同,得定义 struct Point<T, U>.

3. 泛型方法

impl 块中使用泛型:

1impl<T> Point<T> {
2    fn x(&self) -> &T {
3        &self.x
4    }
5}

如果你只想给特定类型实现方法:

1impl Point<f32> {
2    fn distance(&self) -> f32 { ... }
3}

这样只有 Point<f32> 才有 distance 方法,Point<i32> 没有。

4. 单态化 (Monomorphization)

Rust 的泛型性能极高,因为编译器会在编译时将泛型代码展开成具体类型的代码。

例如,如果你用了 Point<i32>Point<f64>,编译器会生成两个不同的结构体(类似 Point_i32Point_f64),并把代码中的泛型调用替换成具体的函数调用。

这意味着:

  1. 没有运行时开销(不需要像 Java 那样装箱/拆箱,也不需要运行时查虚表)。
  2. 二进制体积会膨胀(同样的逻辑生成了多份机器码)。

5. 小结

第十四篇笔记。

  • 泛型让我们可以写出通用代码,避免重复。
  • Trait Bounds 用来限制泛型必须具备的行为(下一章详解)。
  • 单态化 保证了泛型的执行效率。

下一篇,我们将深入讲解 Traits。泛型定义了"参数化类型",而 Traits 定义了"类型的行为约定",两者结合才是 Rust 面向接口编程的完全体。


练习题

  1. 定义一个泛型结构体 Stack<T>,实现 pushpop 方法。
  2. 尝试编写一个函数,接受两个不同类型的点 Point<T>Point<U>,返回一个混合类型的点 PointMixed<T, U>

思考题

Java 的泛型是类型擦除 (Type Erasure) 的,而 Rust 是单态化的。这两种实现方式对性能和运行时行为有什么影响?


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读