📚 Rust 课程系列

  1. 课程概览
  2. 基础语法
  3. 函数与输入输出
  4. 所有权、借用与生命周期
  5. 结构体、枚举与模式匹配
  6. 类型系统:泛型、trait 与多态(本文)
  7. 集合与容器
  8. 错误处理与 Panic 恢复
  9. 模块、属性与宏
  10. 智能指针、迭代器与闭包
  11. 并发与异步编程
  12. Unsafe Rust 与常用 trait 详解
  13. 工具链、Cargo 与外部 crate
  14. 最佳实践、性能与调试

Rust 的类型系统是它内存安全与零成本抽象的根基。本章从类型推断、泛型、trait 出发,逐步讲到 trait bound、关联类型、静态/动态分发的取舍,再到 newtype、型变、PhantomData 等进阶主题,帮你建立对类型系统全局的认识。标准库各 trait 的逐一详解见《Unsafe Rust 与常用 trait 详解》,完整方法签名可参考《Rust 标准 Trait 详解》

类型推断

Rust 编译器能自动推断大多数类型,推断是「双向」的:从使用处和声明处同时收集约束,汇聚出唯一类型;信息不足时要求显式注解。

1
2
3
4
let x = 5;          // i32(默认整型)
let y: f64 = 3.14; // 显式声明
let v = vec![]; // 类型待定
v.push(1i32); // 由 push 推断 v: Vec<i32>

当推断无能为力时(空 Veccollect 目标类型等),用「涡轮鱼」::<T> 或变量注解补全:

1
2
3
4
let v: Vec<i32> = Vec::new();
let nums = "1 2 3".split_whitespace()
.map(|s| s.parse::<i32>().unwrap())
.collect::<Vec<_>>();

💡 提示collect::<Vec<_>>() 中的 _ 让编译器推断元素类型,只需指定容器类型——这是最常用的写法。

泛型

泛型通过单态化(monomorphization)在编译期为每个具体类型生成一份代码,是零运行时成本的抽象。

🔄 对比:Rust 泛型 ≈ C++ 模板(编译期展开),但 Rust 会在定义处检查 trait bound,而非实例化时才报错;Go 泛型也用单态化,但表达能力受接口约束限制。

1
2
3
4
5
6
7
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest { largest = item; }
}
largest
}

泛型可用于函数、结构体、枚举和方法:

1
2
3
4
5
6
7
8
9
10
11
struct Point<T> { x: T, y: T }

impl<T> Point<T> { // <T> 声明泛型参数
fn x(&self) -> &T { &self.x }
}

impl Point<f64> { // 仅为 Point<f64> 实现
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

const 泛型

const 泛型允许把编译期常量作为参数,常用于固定大小数组:

1
2
3
struct Array<T, const N: usize> { data: [T; N] }

fn first<T, const N: usize>(arr: &Array<T, N>) -> &T { &arr.data[0] }

💡 提示:const 泛型让函数直接接受 [T; N] 而非 &[T],保留了数组长度信息。目前仅支持整型、boolchar 等基本类型作为 const 参数。

trait

trait 定义共享行为,类似其他语言的接口:

🔄 对比:Rust trait ≈ Go interface(结构化类型)+ Haskell typeclass,但 Rust 要求显式 impl(不能仅因方法签名匹配就自动满足),这避免了无意中实现 trait 的隐患。

1
2
3
4
5
6
7
8
9
trait Summary {
fn summarize(&self) -> String;
}

struct Article { content: String }

impl Summary for Article {
fn summarize(&self) -> String { format!("Article: {}", self.content) }
}
  • 默认实现:trait 方法可提供默认实现,实现者可覆盖也可直接用:
1
2
3
4
trait Summary {
fn summarize(&self) -> String { String::from("(Read more...)") }
}
impl Summary for Article {} // 使用默认实现
  • 超 trait(supertrait):要求实现某 trait 前必须先实现另一个,用 trait B: A 语法:
1
2
3
4
5
trait OutlinePrint: std::fmt::Display {
fn outline_print(&self) {
println!("* {} *", self.to_string()); // 依赖 Display
}
}

⚠️ 注意:超 trait 的约束在 impl 时由编译器检查——若类型没实现 Display,就无法 impl OutlinePrint

trait bound 与 impl Trait

trait bound 约束泛型使其具备某些能力。约束复杂时用 where 子句提高可读性:

1
2
3
4
5
6
7
8
9
10
11
12
// 内联 bound
fn some_fn<T: Clone + std::fmt::Debug>(x: T) -> T { x.clone() }

// where 子句——约束多时更清晰
fn parse_all<I, T>(iter: I) -> Vec<T>
where
I: IntoIterator<Item = T>,
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
iter.into_iter().map(|s| s.parse().unwrap()).collect()
}

impl Trait 是 bound 的语法糖,让函数签名更简洁:

1
2
3
4
5
6
7
fn print(items: impl IntoIterator<Item = i32>) {  // 等价于 <T: IntoIterator<Item = i32>>
for i in items { println!("{}", i); }
}

fn counter() -> impl Iterator<Item = i32> { // 返回某个实现了 Iterator 的具体类型
(0..5).into_iter()
}

⚠️ 注意impl Trait 作返回值时,函数只能返回单一具体类型——不能在不同分支返回不同类型。闭包类型无法写出名字时 impl Trait 尤其有用。需要多态返回请用 Box<dyn Trait>(见下文)。

关联类型

关联类型把「输出类型」与 trait 实现绑定,由实现方决定,而非调用方指定:

1
2
3
4
5
6
7
8
9
trait Container {
type Item;
fn get(&self, index: usize) -> Self::Item;
}

impl<T: Clone> Container for VecContainer<T> {
type Item = T;
fn get(&self, index: usize) -> Self::Item { self.items[index].clone() }
}

关联类型 vs 泛型参数的关键区别:

维度泛型参数关联类型
谁决定调用方实现方
同一类型可实现次数多次(如 impl From<u8> + impl From<u16>仅一次(如 Iteratortype Item
调用时写法Trait<T=U>Trait(类型已确定)

💡 提示:如果每次使用 trait 都需要指定额外类型参数,用关联类型更简洁;如果同一类型需要多种实现,用泛型参数。

关联常量同理:

1
2
3
4
5
6
trait Greet {
const NAME: &'static str;
fn greet(&self) { println!("Hello, {}!", Self::NAME); }
}
struct Earth;
impl Greet for Earth { const NAME: &'static str = "Earth"; }

静态分发与动态分发:impl Trait vs dyn Trait

静态分发(泛型 / impl Trait动态分发(dyn Trait
时机编译期单态化运行时 vtable 间接调用
开销零运行时开销,但二进制变大一次间接调用开销
集合只能存同构类型可存异构类型
1
2
3
4
trait Draw { fn draw(&self); }

fn draw_static<T: Draw>(item: &T) { item.draw(); } // 静态分发
fn draw_dynamic(item: &dyn Draw) { item.draw(); } // 动态分发

dyn Trait 是动态大小类型(DST),必须以胖指针形式出现——同时记录数据地址和 vtable 地址:

1
2
3
4
5
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle),
Box::new(Square), // 异构集合:动态分发的核心价值
];
for s in &shapes { s.draw(); }

💡 提示:默认用泛型/impl Trait(静态分发);只有当需要在集合中存放不同类型、或想减少二进制体积时才用 dyn Trait

🔄 对比dyn Trait 的 vtable 机制 ≈ C++ 虚函数表,但 Rust 的 trait 对象是显式选择(dyn),而非默认行为;Go 的 interface 值天然是动态分发。

对象安全

并非所有 trait 都能作为 dyn Trait 使用。trait 必须对象安全(object safe)才能构造 trait 对象,要求:

  • 所有方法不能返回 Self(运行时不知道具体类型,无法构造)。
  • 所有方法不能有泛型参数(vtable 是固定签名的,无法为每个泛型实例化单独留槽)。
  • 第一个参数必须是 self/&self/&mut self/Box<Self> 等接收器形式,且不能有额外的泛型约束。

例如 Clone(返回 Self)、Sized(要求编译期已知大小)都不对象安全;Iterator 本身对象安全,可用 Box<dyn Iterator<Item = i32>>

⚠️ 注意:对象安全是编译器在 dyn Trait 使用点检查的,不是 trait 定义处的硬约束——trait 本身可以包含不对象安全的方法,只是不能通过 dyn 调用它们。

newtype 模式

newtype 用元组结构体包裹底层类型,零开销地获得一个全新类型,常用于:

  • 区分语义相同的底层类型(避免把 UserIdSessionId 混用)。
  • 为外部类型实现外部 trait(孤儿规则的常见绕法)。
  • 限制可见性、附加校验。
1
2
3
4
5
6
7
8
9
struct UserId(u32);
struct SessionId(u32);

fn authenticate(user_id: UserId) { /* ... */ }

let uid = UserId(123);
let sid = SessionId(456);
authenticate(uid);
// authenticate(sid); // 编译错误!类型不同,无法混淆

🔄 对比:newtype ≈ Haskell 的 newtype,零开销语义相同;C++ 没有等价物(强 typedef 提案未通过),只能用继承或宏模拟。

更完整的 newtype 与类型状态机案例见《最佳实践、性能与调试》

类型别名

使用 type 关键字创建类型别名,提高代码可读性。别名不创建新类型(与 newtype 不同),不会带来类型安全:

1
2
type Kilometers = i32;       // Kilometers 和 i32 完全等价
type Result<T> = std::result::Result<T, String>;

⚠️ 注意Kilometersi32 可互换——编译器不区分。需要类型安全请用 newtype。

Never 类型 !

! 是「永不返回」的发散类型。它可以强制转换为任意类型,因此 panic!returnbreakloop {} 等表达式能与其他值类型统一:

1
2
3
4
5
6
let x: i32 = match value {
Some(v) => v,
None => return, // return 的类型是 !,可转为 i32
};

let y = if cond { 1 } else { panic!("oops") }; // panic! 类型是 !,y: i32

💡 提示! 的「可转为任意类型」特性让 match/if 各分支类型统一变得自然——不需要额外包装。未来 ! 将稳定为完整类型,可用于「不可能发生的状态」(如 enum Void {} 的替代)。

Sized?Sized

Sized 是标记 trait,表示编译期已知大小。所有泛型参数默认隐式绑定 T: Sized?Sized 表示「可能不是 Sized」,允许泛型接受动态大小类型(DST)如 [T]strdyn Trait

1
2
fn foo<T>(x: &T) {}        // T 必须是 Sized(默认)
fn bar<T: ?Sized>(x: &T) {} // T 可以是 DST,&str、&[u8]、&dyn Trait 都能传入

💡 提示Box<T>Rc<T>&T&mut T 内部都用了 T: ?Sized,所以它们能持有 DST。你日常写泛型时几乎不需要手动加 ?Sized,除非要写接受 dyn Traitstr 的通用函数。

递归类型与 Box

枚举或结构体若直接包含自身,大小无法在编译期确定(无限嵌套)。用 Box 把递归字段放到堆上,给出固定大小的指针即可打破:

1
2
3
4
5
6
7
8
9
enum List {
Cons(i32, List), // 错误:大小无限
}

// 正确:用 Box 断开
enum List {
Cons(i32, Box<List>),
Nil,
}

💡 提示Box 在此的作用是提供固定大小的间接层(指针大小恒定),而非「堆分配」本身。Box 的更多用法见《智能指针、迭代器与闭包》

型变(Variance)(进阶)

型变描述「子类型关系如何沿着泛型/引用传播」。Rust 中生命周期存在子类型关系:'static 是任意 'a 的子类型(活得更长的引用可以替代活得较短的)。

型变含义Rust 示例
协变子类型关系与 T 同向&'a T'aT 协变——&'static str 可赋给 &'a str
逆变子类型关系与 T 反向fn(T) -> R 对参数 T 逆变
不变子类型关系不传播&mut TT 不变——防止通过可变引用写入短生命周期值后让长生命周期引用悬空
1
2
3
4
5
6
let s: &'static str = "hello";
let r: &'a str = s; // 协变:'static -> 'a 允许

let mut v: &'static str = "hi";
// 若 &mut T 协变,就能把 &'a str 写入 v,使 v 指向短命数据 -> 不安全
// 所以 &mut T 对 T 不变,这种写入被拒绝

💡 提示:型变通常由编译器自动处理,但在使用 unsafe、设计含生命周期的 API 时理解它至关重要。Cell<T>UnsafeCell<T> 内部可变性也是型变系统的关键(Cell<T>T 不变)。

🔬 进阶:为什么 &mut T 必须不变?假设 &mut &'static str 能协变为 &mut &'a str,你就可以把 &'a str 写入其中,但外部仍以为它是 &'static str——生命周期被「拉长」,悬空引用由此产生。不变性是借用检查器安全保证的数学基础。

PhantomData(进阶)

PhantomData<T> 是零大小的标记类型,告诉编译器「假装我拥有/使用了 T」,从而影响型变、drop 检查与生命周期约束,而不实际存储 T

1
2
3
4
5
6
7
use std::marker::PhantomData;

// 类型参数 T 未出现在字段中,需要 PhantomData 占位
struct TaggedVec<T> {
data: Vec<u8>,
_marker: PhantomData<T>,
}

💡 提示:标准库中 Box<T>PhantomData<T> 表达对 T 的所有权语义(正确参与 drop 检查),Rc<T>PhantomData<*const T> 表达「非拥有但参与型变」。PhantomData 的实战应用(类型状态机)见《最佳实践、性能与调试》

🔬 进阶PhantomData<T> 让结构体对 T 协变,PhantomData<*const T> 让它不变(因为裸指针没有所有权语义,编译器保守选择不变)。选择哪种取决于你的类型是否应该「接受子类型替换」。

类型转换(简述)

Rust 不支持隐式类型转换,转换都通过 trait 显式完成,避免意外的精度丢失或语义变化:

trait用途失败时
From/Into不会失败的转换
TryFrom/TryInto可能失败的转换返回 Result
AsRef/AsMut廉价的引用转换 &T -> &U
as基本数值类型强制转换可能截断(静默)
1
2
3
4
let n: i32 = "42".parse().unwrap();      // FromStr
let s: String = n.to_string(); // ToString
let bytes: &[u8] = "hi".as_bytes(); // AsRef
let f = 5_i32 as f64; // 数值 as 转换

⚠️ 注意as 转换可能静默截断(如 300_i32 as u8 得到 44),建议优先用 try_into() 让截断显式化。各转换 trait 的完整定义与实现要点见《Rust 标准 Trait 详解》From 在错误传播中的关键作用见《错误处理与 Panic 恢复》


延伸阅读:Rust 标准 Trait 详解(标记/派生/操作/迭代/IO/错误 trait 的逐一讲解与代码)。