首页 / Rust 入门教程 / 泛型(Generic Types)

Rust 入门教程

泛型(Generic Types)

本教程共 78 篇 · 第 43 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程泛型Generictrait 约束单态化const 泛型

本节目标:用泛型让一套函数、结构体、枚举适配多种类型,理解为什么泛型参数需要 trait 约束,以及 Rust 泛型为何”零成本”。

写两个数相加,整数要一份、浮点要一份、自定义类型再来一份——重复得让人头大。泛型就是为解决这个而生的:一个”通用炮管”,什么类型的”炮弹”都能打。

从更高处看,泛型其实是”多态”的一种:同一份逻辑,适配多种类型。它最大的价值不是少写几行,而是”只维护一份源码,却能正确作用于无数类型”,而且——下面会讲——运行时零开销。

1-1 泛型函数初见

没有泛型时,得为每种类型写一个函数:

fn add_i8(a: i8, b: i8) -> i8 { a + b }
fn add_i32(a: i32, b: i32) -> i32 { a + b }
fn add_f64(a: f64, b: f64) -> f64 { a + b }

用泛型,一个搞定:

fn add<T>(a: T, b: T) -> T {
    a + b
}

<T> 是泛型参数声明,T 是类型占位符(惯例用 T,取自 type)。用之前必须先声明 add<T>,随后参数和返回值都能用 T

Note

上面这段其实编不过,因为 T 不一定是能相加的类型。这点下一节讲。先对泛型有个直观印象即可。

1-2 泛型需要 trait 约束

T 可以是任何类型,但不是所有类型都能比较、能相加。所以编译器要求你对 T 加限制,这就是trait 约束

use std::cmp::PartialOrd;

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

T: PartialOrd 表示”T 必须实现 PartialOrd 这个 trait(即可比较)“。不满足的类型,编译器会直接拦下,还贴心地提示你加约束。

回到开头的 add,要能相加得约束 Add

use std::ops::Add;

fn add<T: Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

Add<Output = T> 表示加法的结果类型还是 T。加上约束后就能编过了。

1-3 结构体里的泛型

结构体字段也能用泛型,比如一个能装任意类型坐标的点:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

注意要提前声明 Point<T>。这里 xy 必须是同一类型——一旦 x 被推断成整数,y 也得是整数,否则报错。

想让 xy 类型不同,就上两个不同的泛型参数:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let p = Point { x: 1, y: 1.1 };
}
Tip

泛型参数别贪多。要是写出 struct Woo<T,U,V,W,X>,说明这个结构体该拆了。参数越多,代码越难读。

1-4 枚举里的泛型

你早已见过两个带泛型的枚举。Option<T>T 装可能存在的值:

enum Option<T> {
    Some(T),
    None,
}

Result<T, E>T 装成功值、用 E 装错误:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

正是泛型让这两个枚举能套在任何类型上,成为 Rust 错误处理的基石。

1-5 方法里的泛型

在带泛型的结构体上实现方法,要在 impl 后也声明泛型:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl<T> 的声明让编译器知道尖括号里是泛型。这里 Point<T> 已经是个完整类型,不再是声明。

方法还能有自己额外的泛型参数,和结构体上的不冲突:

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point { x: self.x, y: other.y }
    }
}

T, U 是结构体的泛型,V, W 是方法 mixup 自己的泛型。

甚至可以为某个具体类型单独实现方法:

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

只有 Point<f32> 才有这个方法,其他 TPoint 没有。

1-6 const 泛型:针对”值”的泛型

前面的泛型都针对”类型”。数组 [i32; 3][i32; 2] 是不同类型,想写一个函数打印任意长度数组,光靠类型泛型不够。这时用 const 泛型,针对”值”(这里是长度):

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}

fn main() {
    let arr1 = [1, 2, 3];
    let arr2 = [1, 2];
    display_array(arr1);
    display_array(arr2);
}

const N: usize 表示”值泛型 N”,用来替代数组长度。const 泛型从 Rust 1.51 起稳定,写固定大小的缓冲区、矩阵时特别有用。

1-7 零成本:单态化

你可能会担心:泛型会不会拖慢运行速度?答案是不会。Rust 在编译期做单态化(monomorphization):编译器找出所有用到泛型的地方,为每种具体类型生成一份专属代码。

比如 Some(5)Some(5.0),编译器实际会生成 Option_i32Option_f64 两套定义。运行时执行的,就跟你手写每个具体版本一模一样——没有任何性能损耗。

代价是编译时间变长、最终二进制稍大。换来的是”写一份代码、跑出专属性能”,这就是 Rust 津津乐道的”零成本抽象”。

1-8 泛型约束的写法变体

随着约束变多,把 T: Trait 全写在一对尖括号里会越来越挤。Rust 提供两种等价的写法,挑读起来顺的用。

其一是内联写法:fn f<T: Clone + std::fmt::Debug>(x: T)。其二是 where 子句,把约束挪到函数签名之后,适合约束长或涉及多个类型参数的情况:

fn f<T, U>(x: T, y: U)
where
    T: Clone + std::fmt::Debug,
    U: std::fmt::Display,
{
    println!("{:?} / {}", x, y);
}

为结构体或枚举实现方法时,泛型参数要紧跟 impl 写一遍,再在类型上写一遍:impl<T> Wrapper<T> { ... }。如果一个方法只对”满足某约束的 T”才有意义,就在这个方法自己的 impl 块上加约束——这叫”条件实现”,后面章节会展开。

Note

单态化意味着每个用到的具体类型都会生成一份独立代码。所以泛型不是”运行时万能盒子”,而是”编译期为每种类型各生成一份专用代码”,既通用又零成本。

1-9 const 泛型与固定大小数组

除了”对类型”的泛型,Rust 还有”对值”的泛型——const 泛型,最典型就是数组的长度。你可以写出”长度也是参数”的结构体:

struct Buffer<T, const N: usize> {
    data: [T; N],
}

impl<T, const N: usize> Buffer<T, N> {
    fn len(&self) -> usize { N }
}

这里 N 是编译期已知的常量,所以 Buffer<i32, 8>Buffer<i32, 16> 是两个不同类型,编译器能为各自生成专用代码,且数组大小在编译期就固定,没有运行时堆分配。

这和普通 Vec<T> 形成对比:Vec 的长度是运行期可变的、数据在堆上;数组 [T; N] 的长度在编译期定死、数据通常在栈上。需要”大小固定又能通用”时,const 泛型最合适。

Note

const 泛型不限于 usize 长度,也可以是其它整数类型或 bool 等常量,但入门阶段记住”数组长度参数化”这一个用法就够用了。

1-10 泛型与 trait 一起出现的经典模式

泛型单独用已经能省代码,但真正威力爆发是在它和 trait 约束合用的时候。一个”既通用又安全”的函数长这样:类型参数 T 负责”通吃各种类型”,trait 约束负责”限定 T 必须会做什么”,两者配合才既灵活又不会写出对 T 一无所知、编译不过的代码。

回忆求最大元素的例子:fn largest<T: PartialOrd + Copy>(list: &[T]) -> TT 让整数、浮点、字符都能用同一份逻辑,PartialOrd 保证能比较、Copy 保证能复制返回。缺了约束,编译器不知道 T 能不能比大小,直接报错——这就是”约束定义能力”的体现。

还有一个心智模型:当你发现自己在为 i32f64String 各写一份几乎一样的函数时,就该问”能不能提成泛型”。提成泛型后,单态化会为每种类型各生成专用代码,性能和手写一模一样,却只维护一份源码。

Tip

泛型不是越多越好。只给”确实需要通吃多种类型”的地方用泛型;如果永远只有一种类型,写死反而更清楚。

1-11 当编译器推断不出类型:显式标注

大多数时候编译器能从参数反推出 T 是什么。但有些情况它做不到,比如函数本身没有参数、或者某个值能转成多种类型时。这时要手动告诉它 T 是谁,语法是在函数名后加 ::<类型>,江湖人称 “turbofish”:

fn make<T>() -> Vec<T> {
    Vec::new()
}

fn main() {
    let v = make::<i32>();   // 明确 T 是 i32
    println!("{}", v.len());
}

make::<i32>() 相当于喊一声”这里 T 就是 i32”。更常见的场景是 collect:它既能收成 Vec 也能收成 HashMap,编译器需要你点明:

use std::collections::HashMap;

let v = vec![(1, "a"), (2, "b")];
let m: HashMap<i32, &str> = v.into_iter().collect(); // 类型标注也能替代 turbofish

那为什么之前第 38 章我们写的是 collect::<HashMap<_, _>>()?因为 collect 的目标类型要靠”上下文”推断,而用 ::<HashMap<_, _>> 显式点明最稳。本质上,类型标注 : HashMap<...> 和 turbofish ::<...> 是同一件事的两种写法,挑顺手的就行。

Note

记住 turbofish 这个写法 ::<T>:遇到”编译器说推断不出泛型”的报错时,它几乎总是解药。

1-12 小结

  • 泛型用 <T> 声明,让函数/结构体/枚举适配多种类型;
  • 泛型参数常需 trait 约束(如 T: PartialOrd),否则编译器不知道能做什么操作;
  • 结构体、枚举、方法都能用泛型;还能为具体类型单独实现方法;
  • const 泛型 const N: usize 针对”值”,常用来处理数组长度;
  • 泛型通过编译期单态化实现零成本抽象,运行时无开销。

下一章看 trait,它是 Rust 表达”共享行为”的核心机制,也正是泛型约束要引用的那些 trait。