泛型与 Trait
泛型和 trait 是 Rust 实现代码复用与多态的两大支柱。前置:Rust 基础。泛型让代码可以工作在多种类型上而不牺牲性能,trait 定义了类型间的共享行为,二者结合形成了零成本抽象的强大表达能力。本章将深入泛型定义、trait 的设计实现、标准库中最重要的 trait,以及一些高级模式。
6.1 泛型
泛型允许你编写适用于多种类型的代码,同时编译器通过单态化为每个使用的具体类型生成特化版本,确保零运行时开销。
函数泛型
在函数签名中使用泛型参数 T,并为它附加 trait 约束以明确 T 必须支持的操作。
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
T 后面的 : PartialOrd 是 trait 约束,表示类型 T 必须可比较大小。多个泛型参数用逗号分隔,也可用 where 子句提升可读性:
fn some_fn<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{ ... }
结构体与枚举泛型
结构体可以存储任意类型的数据:
struct Point<T> {
x: T,
y: T,
}
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
枚举同样支持泛型,标准库中的 Option<T> 和 Result<T, E> 就是最典型的例子。可以用多个泛型参数表示不同类型的字段:
struct Pair<T, U> {
a: T,
b: U,
}
方法中使用泛型
为泛型类型实现方法时,需要在 impl 后声明泛型参数:
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
可以针对特定类型或满足特定约束的类型单独实现方法。例如,只有 Point<f64> 才提供 distance_from_origin:
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
也可以在 impl 上附加 trait 约束,使得方法对所有满足约束的类型可用:
impl<T: Display + PartialOrd> Point<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("x = {}", self.x);
} else {
println!("y = {}", self.y);
}
}
}
单态化
调用泛型代码时,编译器为每一个使用的具体类型生成一份专用的副本,这个过程叫单态化。例如 largest(&[1, 2, 3]) 会被编译为一份处理 i32 的 largest 函数,与手写非泛型函数拥有相同的性能。代价是编译时间增加,且可能产生更大的二进制文件。
泛型参数默认类型
在 trait 中定义泛型参数时,可以指定默认具体类型,减少显式标注。典型例子是标准库的 Add trait:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
当写 a + b 时,如果没有显式指定 Rhs,默认就是 Self,即同类型相加。但你可以实现不同 Rhs 来实现异类型相加。
6.2 Trait(特质)
trait 是 Rust 中的共享行为抽象,告诉编译器不同类型共同拥有的功能。它类似于其他语言中的接口,但更强大。
定义 trait
定义一个 Summary trait,包含一个方法签名,也可带默认实现:
pub trait Summary {
fn summarize(&self) -> String;
}
也可以提供默认方法体,实现者可用也可覆写:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
实现 trait
为某个类型实现 trait 的语法是 impl Trait for Type:
pub struct NewsArticle {
pub headline: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}: {}", self.headline, &self.content[..100])
}
}
孤儿规则
Rust 的 trait 实现受孤儿规则限制:如果要实现一个 trait 或为某个类型实现 trait,至少其中一个必须在当前 crate 中定义。这防止了上游 crate 添加新 trait 实现后破坏下游代码。换句话说,不能同时为外部类型实现外部 trait。
要绕开这一限制,可以使用新类型模式(newtype pattern),在后文介绍。
trait 作为参数
可以用 impl Trait 语法或 trait bound 语法指定要求参数的 trait。
impl Trait 形式:
fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}
trait bound 形式(显式泛型):
fn notify<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
当参数类型复杂时,impl Trait 更简洁;当需要多个参数为同一类型或复杂泛型关系时,trait bound 更合适。
trait 作为返回值
可以让函数返回某个实现了指定 trait 的类型,而不暴露具体类型:
fn returns_summarizable() -> impl Summary {
NewsArticle {
headline: String::from("Breaking News"),
content: String::from("Something happened..."),
}
}
注意:impl Trait 在返回值位置时,函数只能返回单一具体类型,不能根据条件返回不同的实现类型。如果确实需要不同具体类型,就得使用 trait 对象。
dyn Trait:trait 对象
trait 对象用于动态分发,即运行时决定具体方法。类型写作 &dyn Trait 或 Box<dyn Trait>:
fn notify(item: &dyn Summary) {
println!("{}", item.summarize());
}
let article = NewsArticle { ... };
notify(&article);
不同于静态分发的泛型,使用 trait 对象会有小幅运行时开销(虚表查找),且对象的大小在编译期未知,因此通常放在指针后。trait 对象要求使用的 trait 必须是对象安全的,即其方法不能返回 Self 类型、不能有泛型参数(除了关联类型受某些限制等)。
关联类型
关联类型是在 trait 定义中设定的类型占位符,具体类型由实现者指定。它们简化了复杂的泛型 trait 签名。最经典的例子是 Iterator trait:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
实现时指定 Item 具体类型:
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> { ... }
}
关联类型使 Iterator 不需要像 Iterator<Item=T> 那样每次都要标注泛型,接口更干净。
6.3 常用标准 Trait
Rust 标准库提供了一系列基础 trait,它们是类型系统进行数据操作和生态互操作的基石。
格式化:Display 与 Debug
Debug:面向开发者的调试输出,{:?}格式串使用。通常通过#[derive(Debug)]自动生成。Display:面向用户的格式化输出,{}使用,需要手动实现fmt方法。
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
Clone 与 Copy
Clone:显式深拷贝,调用.clone()方法。Copy:隐式浅拷贝(按位复制),标记类型,要求类型不涉及资源所有权。简单的标量、无分配的聚合类型通常派生Copy, Clone。
#[derive(Copy, Clone)]
struct Color { r: u8, g: u8, b: u8 }
实现 Copy 必须同时实现 Clone。
比较:PartialEq / Eq 与 PartialOrd / Ord
PartialEq:允许部分等价关系(如f64的NaN != NaN),提供==和!=。Eq:标记 trait,表示等价关系完全,无部分性。通常也通过derive实现。PartialOrd:部分顺序,提供<、>等。Ord:全序关系,要求类型形成严格的全序。
Hash
实现 Hash 的类型可被哈希,用于 HashMap 的键。可以通过 derive 自动生成,只要所有字段都实现 Hash。
Default
提供类型的默认值。很多集合(如 Vec)使用 Default 来初始化。
#[derive(Default)]
struct Config {
port: u16,
debug: bool,
}
let config = Config { port: 8080, ..Default::default() };
From / Into
类型转换 trait。实现了 From<A> for B,会自动得到 Into<B> for A。推荐手动实现 From,因为 Into 由标准库通用实现自动生成。
impl From<i32> for MyNumber {
fn from(val: i32) -> Self {
MyNumber(val)
}
}
let num: MyNumber = 42.into();
? 运算符在错误传播时也依赖 From 来转换错误类型。
Deref / DerefMut
实现解引用操作符 *。Deref 让智能指针能当普通引用来使用;DerefMut 用于可变解引用。过度使用可能破坏封装性,主要应用在智能指针类型上。
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
Drop
析构器,值离开作用域时会自动调用 drop 方法,用于释放资源、关闭文件等。不能手动调用,但可以用 std::mem::drop 提前转移所有权并触发析构。
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping!");
}
}
Iterator
迭代器的核心 trait,实现了 Iterator 的类型就能使用 map、filter、collect 等一整套适配器方法。只需要实现 next 方法,关联类型 Item 指定迭代出的元素类型。
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 { Some(self.count) } else { None }
}
}
标准库为所有迭代器提供了大量可用方法,零成本抽象。
6.4 高级 Trait
Supertrait(超 trait)
一个 trait 可能依赖于另一个 trait 的功能,此时可以使用超 trait 语法 trait Foo: Bar 表示实现 Foo 的类型必须同时实现 Bar。
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
println!("{}", "*".repeat(output.len() + 4));
println!("*{}*", " ".repeat(output.len() + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(output.len() + 2));
println!("{}", "*".repeat(output.len() + 4));
}
}
这样 OutlinePrint 的方法就可以调用 self.to_string(),因为它要求 Display。
完全限定语法
当多个 trait 或类型自身拥有同名方法时,需要使用完全限定语法消除歧义:
<Type as Trait>::function(receiver_if_method, args...);
例如,一个类型同时实现了两个有同名方法的 trait:
trait Pilot { fn fly(&self); }
trait Wizard { fn fly(&self); }
struct Human;
impl Pilot for Human {
fn fly(&self) { println!("Captain speaking."); }
}
impl Wizard for Human {
fn fly(&self) { println!("Up!"); }
}
impl Human {
fn fly(&self) { println!("Waving arms."); }
}
let person = Human;
person.fly(); // 调用 Human 自身方法
Pilot::fly(&person); // 调用 Pilot trait 方法
Wizard::fly(&person); // 调用 Wizard trait 方法
对于关联函数(无 self),需要用 <Human as Wizard>::fly() 形式。
新类型模式(newtype pattern)
孤儿规则阻止我们为外部类型实现外部 trait。新类型模式通过定义一个元组结构体包装外部类型,使它能获得本地的身份,从而可以安全地实现外部 trait。
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
Wrapper 是本地类型,因此可以为它实现 Display。使用时通过 Wrapper 包装和解构访问内部值。
标记 trait:Send, Sync, Sized, Unpin
这些较为特殊的 trait 通常由编译器自动推导,或者在 unsafe 中手动实现。
Send:类型的所有权可以安全地转移到另一个线程。大多数类型都是Send,但Rc<T>不是。Sync:类型的共享引用可以安全地在多个线程间访问。基本类型是Sync,RefCell<T>不是。Sized:编译期已知大小的类型。默认泛型参数有隐性Sized约束,如果要放宽,可用T: ?Sized。Unpin:表示类型可以安全地在内存中移动而不破坏内部指针。大多数类型自动实现Unpin,Pin相关的内容用于异步编程和自引用结构体。
这些标记 trait 构成了 Rust 强大并发和内存安全保证的一部分,会在高级主题中进一步详细应用。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| 泛型 | 官方书第 10 章 |
| Trait | 官方书 |
| Rust Reference · Traits | 参考 |
相关文章
第八章:测试、文档与质量
Rust 将测试和文档视为语言的一等公民。通过内置的测试框架,你可以在项目里编写单元测试、集成测试以及随文档一起运行的示例测试,三者共用 cargo test 命令。配合 cargo doc 生成文档,形成了一套确保代码质量与可维护性…
第二章:基础语法与类型
Rust 的类型系统和语法设计处处体现着“安全”与“显式”的理念。这一章你将掌握变量绑定、基本类型、复合类型、函数定义以及所有基础控制流结构,它们是你写出任何 Rust 程序的基石。
第七章:模块系统与包管理
Rust 的模块系统为代码组织、封装和复用提供了一套严谨但灵活的机制。包、crate、模块以及 use 路径相互配合,让你能够把项目拆解成清晰的功能单元,同时精确控制哪些对外可见。本章将带你系统掌握这些构建大型 Rust 项目所必需的…
第十章:进阶特性与模式
Rust 的核心安全保证覆盖了绝大多数日常编程场景。但当你需要打破常规——无论是编写极致通用的抽象、与 C 库交互,还是内联优化——本章将带你进入 Rust 的深层能力:声明宏与过程宏、unsafe 的超能力与封装、高级类型系统技巧…
第五章:错误处理
Rust 将错误明确分为两类:不可恢复的错误与可恢复的错误。通过 panic! 处理前一种,Result 处理后一种,这让程序的错误路径不再是隐式的控制流,而是强类型、必须处理的代码分支。配合 Option 对缺失值的处理以及丰富的组…
第三章:所有权、借用与生命周期(核心)
所有权系统是 Rust 最独特、最核心的语言特性。它让 Rust 无需垃圾回收器就能保证内存安全,并在编译期消除数据竞争。本章你将深入理解所有权如何运转、引用和借用如何被检查、生命周期如何标注,以及常见智能指针如何扩展所有权模型。
Series
rust
9 / 10