跳到主要内容

泛型与 Trait

泛型是编写可复用代码的核心特性,而 Trait 定义了共享行为。两者结合可以实现灵活且类型安全的抽象。理解这两个概念是掌握 Rust 高级编程的关键。

为什么需要泛型?

在没有泛型的情况下,如果我们想写一个找出最大值的函数,可能需要为每种类型都写一个版本:

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

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

这两个函数的逻辑完全相同,只是类型不同。泛型让我们可以只写一次:

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

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("最大数字: {}", result);

let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("最大字符: {}", result);
}

泛型的核心优势

  • 代码复用:同一份代码适用于多种类型
  • 类型安全:编译时检查类型约束
  • 零成本抽象:Rust 通过单态化消除泛型的运行时开销

泛型数据类型

函数中的泛型

泛型函数使用类型参数来接受不同类型的输入:

// T 是类型参数,可以是任何名称,但惯例使用 T
fn first<T>(list: &[T]) -> &T {
&list[0]
}

fn main() {
let numbers = vec![1, 2, 3];
let first_number = first(&numbers);

let chars = vec!['a', 'b', 'c'];
let first_char = first(&chars);
}

结构体中的泛型

结构体可以使用泛型来定义灵活的数据结构:

// 单个类型参数:x 和 y 必须是相同类型
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 };

// 编译错误:x 和 y 必须是相同类型
// let mixed = Point { x: 5, y: 4.0 };
}

// 多个类型参数:x 和 y 可以是不同类型
struct PointMulti<T, U> {
x: T,
y: U,
}

fn main() {
let p1 = PointMulti { x: 5, y: 10 }; // 两个都是 i32
let p2 = PointMulti { x: 1.0, y: "hello" }; // f64 和 &str
let p3 = PointMulti { x: "left", y: 'c' }; // &str 和 char
}

枚举中的泛型

标准库中最常见的泛型枚举是 OptionResult

// Option 表示值可能存在或不存在
enum Option<T> {
Some(T),
None,
}

// Result 表示操作成功或失败
enum Result<T, E> {
Ok(T),
Err(E),
}

// 自定义泛型枚举
enum Container<T> {
Single(T),
Pair(T, T),
Triple(T, T, T),
}

fn main() {
let single = Container::Single(42);
let pair = Container::Pair(1, 2);
let triple = Container::Triple('a', 'b', 'c');
}

方法定义中的泛型

可以在 impl 块中使用泛型来为泛型类型添加方法:

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

// 为所有 Point<T> 实现方法
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}

fn y(&self) -> &T {
&self.y
}
}

// 只为特定类型实现方法
impl Point<f32> {
// 这个方法只对 Point<f32> 可用
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p = Point { x: 5, y: 10 };
println!("x = {}", p.x()); // 所有类型都可以调用

let p_float = Point { x: 1.0, y: 2.0 };
println!("距离原点: {}", p_float.distance_from_origin()); // 只有 f32 可以调用

// p.distance_from_origin(); // 错误:i32 没有这个方法
}

泛型默认类型

可以为泛型参数指定默认类型:

// 为泛型参数指定默认类型
struct Point<T = i32> {
x: T,
y: T,
}

fn main() {
// 使用默认类型 i32
let p1 = Point { x: 1, y: 2 };

// 指定其他类型
let p2: Point<f64> = Point { x: 1.0, y: 2.0 };
}

泛型的性能:单态化

Rust 在编译时会进行单态化(monomorphization),将泛型代码转换为具体类型的代码:

// 编写时
let integer = Some(5);
let float = Some(5.0);

// 编译后(概念上)
enum Option_i32 {
Some(i32),
None,
}

enum Option_f64 {
Some(f64),
None,
}

let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);

单态化的优点

  • 零运行时开销:泛型代码与手写具体类型代码性能相同
  • 编译器可以进行更多优化

单态化的缺点

  • 编译时间更长
  • 二进制文件可能更大

Trait:定义共享行为

Trait 类似于其他语言中的接口(interface),它定义了一组方法,类型可以实现这些方法来提供特定的行为。

定义和实现 Trait

// 定义 Trait
pub trait Summary {
fn summarize(&self) -> String;
}

// 为类型实现 Trait
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

pub struct SocialPost {
pub username: String,
pub content: String,
}

impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

fn main() {
let article = NewsArticle {
headline: String::from("重大新闻"),
location: String::from("北京"),
author: String::from("记者张三"),
content: String::from("今日发生了一件大事..."),
};

println!("{}", article.summarize());

let post = SocialPost {
username: String::from("user123"),
content: String::from("今天天气真好"),
};

println!("{}", post.summarize());
}

Trait 实现的规则

  • 必须先定义 Trait 或类型(至少一个在当前 crate 中)
  • 实现 Trait 时必须实现所有没有默认实现的方法

默认实现

Trait 可以为某些方法提供默认实现:

pub trait Summary {
// 带默认实现的方法
fn summarize(&self) -> String {
String::from("(阅读更多...)")
}

// 没有默认实现,必须实现
fn summarize_author(&self) -> String;

// 调用其他方法的默认实现
fn summarize_with_author(&self) -> String {
format!("{} ({})", self.summarize(), self.summarize_author())
}
}

impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}

// 覆盖默认实现
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
self.author.clone()
}
// summarize 使用默认实现
}

fn main() {
let post = SocialPost {
username: String::from("user123"),
content: String::from("你好世界"),
};

println!("{}", post.summarize()); // 自定义实现
println!("{}", post.summarize_with_author()); // 默认实现,调用 summarize_author
}

Trait 作为参数

可以使用 Trait 作为函数参数,接受任何实现了该 Trait 的类型:

pub trait Summary {
fn summarize(&self) -> String;
}

// impl Trait 语法:简洁直观
pub fn notify(item: &impl Summary) {
println!("突发新闻! {}", item.summarize());
}

// Trait Bound 语法:等价写法,更灵活
pub fn notify2<T: Summary>(item: &T) {
println!("突发新闻! {}", item.summarize());
}

fn main() {
let article = NewsArticle { /* ... */ };
notify(&article);
notify2(&article);
}

impl Trait vs Trait Bound

  • impl Trait 更简洁,适合简单场景
  • Trait Bound 更灵活,适合复杂约束

多个 Trait Bound

当需要多个 Trait 约束时:

use std::fmt::Display;

// impl Trait 语法
pub fn notify(item: &(impl Summary + Display)) {
println!("摘要: {}", item.summarize());
println!("显示: {}", item);
}

// Trait Bound 语法
pub fn notify2<T: Summary + Display>(item: &T) {
println!("摘要: {}", item.summarize());
println!("显示: {}", item);
}

// where 子句:更清晰的复杂约束
pub fn some_function<T, U>(t: &T, u: &U) -> String
where
T: Display + Clone,
U: Clone + Summary,
{
format!("{} - {}", t, u.summarize())
}

什么时候使用 where 子句?

  • 当有多个泛型参数时
  • 当每个参数有多个 Trait 约束时
  • 当函数签名变得难以阅读时

返回实现了 Trait 的类型

可以使用 impl Trait 语法指定返回类型:

pub trait Summary {
fn summarize(&self) -> String;
}

// 返回实现了 Summary 的类型
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("user"),
content: String::from("内容"),
}
}

fn main() {
let post = returns_summarizable();
println!("{}", post.summarize());
}

重要限制:只能返回单一类型

// 错误:不能返回不同类型
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle { /* ... */ } // 类型 A
} else {
SocialPost { /* ... */ } // 类型 B
}
}

RPIT 生命周期捕获规则(Rust 2024)

RPIT(Return Position Impl Trait)是指在返回类型位置使用 impl Trait。这是 Rust 中一个重要的抽象机制,允许函数隐藏具体的返回类型。从 Rust 2024 开始,生命周期捕获规则发生了重要变更。

什么是生命周期捕获?

当一个函数返回 impl Trait 时,编译器创建一个"不透明类型"(opaque type)。这个不透明类型可以"捕获"某些泛型参数,使它们可以在隐藏类型中使用。

// 捕获 'a 和 T 意味着返回值可以使用这两个参数
fn example<'a, T>(x: &'a T) -> impl Sized + use<'a, T> {
x // 隐藏类型是 &'a T,使用了 'a 和 T
}
Rust 2024 的核心变更

Rust 2021 及之前

  • 自动捕获所有类型参数和常量参数
  • 只捕获出现在 impl Trait 约束中的生命周期参数

Rust 2024

  • 自动捕获所有作用域内的泛型参数,包括生命周期参数
// 关键示例:理解两个版本的区别
fn foo<'a>(x: &'a ()) -> impl Sized {
// Rust 2021: 等价于 impl Sized + use<>
// 不捕获 'a,所以不能返回 *x

// Rust 2024: 等价于 impl Sized + use<'a>
// 自动捕获 'a,可以返回 *x
*x
}
使用 use<..> 精确控制

use<..> 语法(Rust 1.82+ 引入)允许显式指定捕获哪些参数:

// 显式捕获特定参数
fn capture_explicit<'a, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
(x, y)
}

// 不捕获任何参数
fn capture_none<'a, T>(_x: &'a (), _y: T) -> impl Sized + use<> {
42 // 返回固定值
}

// 只捕获部分参数
fn capture_partial<'a, T>(_x: &'a (), y: T) -> impl Sized + use<T> {
y // 只使用 T,不使用 'a
}
捕获对调用者的影响

捕获的参数会影响不透明类型的使用方式:

fn capture_lifetime<'a>(_: &'a ()) -> impl Sized + use<'a> {}

fn test<'a>(x: &'a ()) -> impl Sized + 'static {
capture_lifetime(x) // 错误!返回类型捕获了 'a,不能是 'static
}

// 解决方案:不捕获生命周期
fn no_capture<'a>(_: &'a ()) -> impl Sized + use<> {}

fn test_ok<'a>(x: &'a ()) -> impl Sized + 'static {
no_capture(x) // 正确!返回类型没有捕获 'a
}
为什么需要这个变更?

这个变更解决了之前开发者需要使用"技巧"来捕获生命周期的问题:

旧方式:Captures 技巧(Rust 2024 之前)

// 定义辅助 trait
trait Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}

// 使用技巧捕获生命周期
fn old_way<'a, T>(x: &'a (), y: T) -> impl Sized + Captures<(&'a (), T)> {
(x, y)
}

新方式:Rust 2024+

fn new_way<'a, T>(x: &'a (), y: T) -> impl Sized {
(x, y) // 自动捕获 'a 和 T,无需额外技巧
}
APIT(Argument Position Impl Trait)的特殊情况

当函数参数使用 impl Trait 时,会创建匿名泛型参数:

fn with_apit<'a>(x: &'a (), y: impl Sized) -> impl Sized {
(x, y)
// Rust 2024: 会捕获 'a 和匿名类型参数
// 如果不想捕获 'a,需要将 APIT 改为命名参数
}

// 显式命名参数以便控制捕获
fn with_named<'a, T: Sized>(x: &'a (), y: T) -> impl Sized + use<T> {
(x, y) // 只捕获 T,不捕获 'a
}
迁移指南

运行 cargo fix --edition 可以自动处理大多数情况:

// 迁移前(Rust 2021)
fn f<'a>(x: &'a ()) -> impl Sized { 42 }

// 自动迁移后(保持旧行为)
fn f<'a>(x: &'a ()) -> impl Sized + use<> { 42 }

迁移建议

  • 如果不需要使用生命周期,编译器会自动添加 use<>
  • 如果代码依赖旧的行为,考虑保留 use<..> 约束
  • 对于新代码,可以利用自动捕获简化代码
Rust 2024 Edition 变更

这个变更是 Rust 2024 Edition 的一部分。详细信息请参考 RPIT Lifetime Capture Rules

实际示例
// Rust 2024+ 推荐写法:利用自动捕获
fn make_stream<'a, T>(data: &'a Vec<T>) -> impl Iterator<Item = &'a T> {
data.iter() // 自动捕获 'a 和 T
}

// 需要控制捕获时的写法
fn conditional_capture<'a, T>(
data: &'a Vec<T>,
use_lifetime: bool,
) -> impl Iterator<Item = &'a T> + use<'a, T> {
data.iter()
}

// 与关联类型结合使用
impl<'a> Parser<'a> {
fn parse(&self) -> impl std::fmt::Display + 'a {
// 返回类型受 'a 约束
self.token
}
}

条件实现

可以为满足特定条件的类型实现 Trait:

use std::fmt::Display;

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

// 为所有 Pair<T> 实现 new
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}

// 条件实现:只有当 T 实现了 Display 和 PartialOrd 时
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("最大的是 x = {}", self.x);
} else {
println!("最大的是 y = {}", self.y);
}
}
}

fn main() {
let pair = Pair::new(10, 20);
pair.cmp_display(); // i32 实现了 Display 和 PartialOrd
}

孤儿规则

Rust 有一条重要的规则叫做孤儿规则(Orphan Rule):实现 trait 时必须满足 trait 或类型至少有一个是本地定义的

// 在本地 crate 中为外部类型实现本地 Trait
// ✓ 允许
impl Summary for Vec<String> {
fn summarize(&self) -> String {
self.join(", ")
}
}

// 在本地 crate 中为本地类型实现外部 Trait
// ✓ 允许
impl Display for SocialPost {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.username)
}
}

// 在本地 crate 中为外部类型实现外部 Trait
// ✗ 禁止
// impl Display for Vec<String> { ... }

为什么需要孤儿规则?

如果没有孤儿规则,两个不同的 crate 可能会为同一个外部类型实现同一个外部 Trait,导致冲突。孤儿规则确保了 Trait 实现的一致性和可预测性。

常用标准 Trait

Clone 和 Copy

#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy:p1 仍然有效

println!("p1: ({}, {})", p1.x, p1.y);
println!("p2: ({}, {})", p2.x, p2.y);
}

Copy vs Clone 的区别

  • Copy:隐式复制,赋值时自动复制
  • Clone:显式复制,需要调用 .clone() 方法
  • Copy 要求类型的所有字段都实现了 Copy

Debug

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}

fn main() {
let person = Person {
name: String::from("张三"),
age: 30,
};

println!("{:?}", person); // 调试格式:Person { name: "张三", age: 30 }
println!("{:#?}", person); // 美化调试格式
dbg!(&person); // 调试宏,输出到 stderr
}

Default

#[derive(Default)]
struct Config {
timeout: u32,
retries: u32,
debug: bool,
}

fn main() {
// 使用默认值
let config = Config::default();
println!("超时: {}, 重试: {}, 调试: {}",
config.timeout, config.retries, config.debug);

// 部分更新
let config = Config {
timeout: 60,
..Default::default()
};
}

From 和 Into

// From trait:定义如何从其他类型转换
#[derive(Debug)]
struct MyNumber(i32);

impl From<i32> for MyNumber {
fn from(value: i32) -> Self {
MyNumber(value)
}
}

// Into trait:自动从 From 实现
// 如果实现了 From<A> for B,则自动有 Into<B> for A

fn main() {
// 使用 From
let num = MyNumber::from(42);
println!("{:?}", num);

// 使用 Into
let num: MyNumber = 42.into();
println!("{:?}", num);

// 实际使用示例
let s = String::from("hello"); // From<&str> for String
let n: i32 = "42".parse().unwrap(); // 返回 Result<i32, ParseIntError>
}

Deref 和 DerefMut

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

fn main() {
let x = MyBox::new(5);
println!("{}", *x); // 解引用:Rust 自动调用 deref()

// Deref 强制转换
fn hello(name: &str) {
println!("Hello, {}!", name);
}

let m = MyBox::new(String::from("Rust"));
hello(&m); // MyBox<String> -> &String -> &str
}

Drop

struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("释放 CustomSmartPointer,数据: {}", self.data);
}
}

fn main() {
let c = CustomSmartPointer {
data: String::from("我的数据"),
};
let d = CustomSmartPointer {
data: String::from("其他数据"),
};
println!("CustomSmartPointer 创建完毕");

// 可以提前释放
drop(c);
println!("c 已被提前释放");
}
// d 在 main 结束时自动释放

Trait 对象

Trait 对象允许在运行时动态地使用实现了特定 Trait 的类型。

静态分发 vs 动态分发

// 静态分发(泛型):编译时确定类型,性能更好
fn draw_static(item: &impl Draw) {
item.draw();
}

// 动态分发(Trait 对象):运行时确定类型,更灵活
fn draw_dynamic(item: &dyn Draw) {
item.draw();
}

使用 Trait 对象

pub trait Draw {
fn draw(&self);
}

pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // 可以存储不同的类型
}

impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

struct Button {
width: u32,
height: u32,
label: String,
}

impl Draw for Button {
fn draw(&self) {
println!("绘制按钮: {} ({}x{})", self.label, self.width, self.height);
}
}

struct TextField {
width: u32,
height: u32,
placeholder: String,
}

impl Draw for TextField {
fn draw(&self) {
println!("绘制文本框: {} ({}x{})", self.placeholder, self.width, self.height);
}
}

fn main() {
let screen = Screen {
components: vec![
Box::new(Button {
width: 50,
height: 10,
label: String::from("确定"),
}),
Box::new(TextField {
width: 100,
height: 20,
placeholder: String::from("输入内容"),
}),
],
};

screen.run();
}

Trait 对象的限制

Trait 对象需要满足对象安全(Object Safety)条件:

// 对象安全的 Trait
trait Draw {
fn draw(&self); // 只有 &self 参数
}

// 不是对象安全的 Trait
trait NotObjectSafe {
fn new() -> Self; // 返回 Self
fn process<T>(&self, item: T); // 泛型方法
}

Trait 对象 vs 泛型

特性泛型(静态分发)Trait 对象(动态分发)
性能编译时确定,更优运行时查表,略差
二进制大小每种类型生成一份代码只有一份代码
灵活性编译时确定类型运行时可混合类型
对象大小编译时已知需要指针间接访问
适用场景类型在编译时已知类型在运行时才确定

选择建议

  • 如果类型在编译时已知,优先使用泛型
  • 如果需要在集合中存储不同类型,使用 Trait 对象
  • 如果性能是关键因素,优先使用泛型

实际应用示例

构建一个简单的日志系统

use std::fmt;

// 定义日志级别
#[derive(Debug, Clone, Copy)]
enum LogLevel {
Info,
Warning,
Error,
}

impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LogLevel::Info => write!(f, "INFO"),
LogLevel::Warning => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
}
}
}

// 日志目标 Trait
trait LogTarget {
fn log(&self, level: LogLevel, message: &str);
}

// 控制台日志
struct ConsoleLogger;

impl LogTarget for ConsoleLogger {
fn log(&self, level: LogLevel, message: &str) {
println!("[{}] {}", level, message);
}
}

// 文件日志(模拟)
struct FileLogger {
filename: String,
}

impl LogTarget for FileLogger {
fn log(&self, level: LogLevel, message: &str) {
println!("[{}] {} -> {}", level, message, self.filename);
}
}

// 日志器
struct Logger {
targets: Vec<Box<dyn LogTarget>>,
}

impl Logger {
fn new() -> Self {
Logger { targets: Vec::new() }
}

fn add_target(&mut self, target: Box<dyn LogTarget>) {
self.targets.push(target);
}

fn log(&self, level: LogLevel, message: &str) {
for target in &self.targets {
target.log(level, message);
}
}

fn info(&self, message: &str) {
self.log(LogLevel::Info, message);
}

fn warning(&self, message: &str) {
self.log(LogLevel::Warning, message);
}

fn error(&self, message: &str) {
self.log(LogLevel::Error, message);
}
}

fn main() {
let mut logger = Logger::new();
logger.add_target(Box::new(ConsoleLogger));
logger.add_target(Box::new(FileLogger {
filename: String::from("app.log"),
}));

logger.info("应用程序启动");
logger.warning("配置文件不存在,使用默认配置");
logger.error("数据库连接失败");
}

小结

本章我们学习了:

  1. 泛型:函数、结构体、枚举、方法中的泛型使用
  2. 单态化:泛型的性能保证
  3. Trait:定义和实现、默认实现、作为参数
  4. Trait Bound:约束泛型类型的行为、where 子句
  5. 标准 Trait:Clone、Copy、Debug、Default、From/Into、Deref、Drop
  6. Trait 对象:动态分发和运行时多态
  7. 孤儿规则:确保 Trait 实现的一致性

练习

  1. 实现一个泛型函数,查找切片中的最小值和最大值
  2. 定义一个 Describe trait,为多种类型实现
  3. 使用 where 子句重写一个复杂的 trait bound 函数
  4. 实现一个可以存储不同类型组件的 Screen 结构体
  5. 为自定义类型实现 FromInto trait

参考资料