9. 常用特征
9.1 简介
Rust 标准库定义了一系列用于描述常见操作的 trait,用于统一不同数据类型的函数接口,使开发者能够以一致的方式操作各种类型,提高代码的灵活性、可复用性和可扩展性。这些 trait 涵盖基本运算、类型转换、错误处理、调试输出、数据复制、比较判断及迭代遍历等场景。
9.2 运算符相关 trait
在 Rust 中,每种运算符(如加、减、乘、除等)都与特定的 trait 绑定,类型必须实现对应的 trait 才能具备相应的运算能力。以下是开发中常用的运算符 trait:
| 运算符 | 特征 | 描述 |
|---|---|---|
+ | Add | 实现加法运算符(+),用于执行加法操作 |
- | Sub | 实现减法运算符(-),用于执行减法操作 |
* | Mul | 实现乘法运算符(*),用于执行乘法操作 |
/ | Div | 实现除法运算符(/),用于执行除法操作 |
% | Rem | 实现取模运算符(%),用于执行模运算操作 |
+= | AddAssign | 实现加法赋值运算符(+=),用于将右侧值加到左侧变量上 |
-= | SubAssign | 实现减法赋值运算符(-=),用于将右侧值从左侧变量中减去 |
*= | MulAssign | 实现乘法赋值运算符(*=),用于将右侧值与左侧变量相乘 |
/= | DivAssign | 实现除法赋值运算符(/=),用于将左侧变量除以右侧值 |
%= | RemAssign | 实现取模赋值运算符(%=),用于将左侧变量对右侧值取模 |
Rust 内置数值类型(如 i32、u32、f64 等)均已实现上述运算符 trait,可直接进行相关运算,也可以为自定义类型实现这些 trait。下述示例为矩形结构体实现 Add trait 后,两个矩形可以通过 + 运算符直接求和。
use std::ops::Add;
// 定义矩形结构体
struct Rectangle {
width: u32,
height: u32,
}
// 为矩形结构体实现 Add trait,使两个矩形可以相加
impl Add for Rectangle {
type Output = Rectangle; // 定义加法操作的结果类型
fn add(self, other: Rectangle) -> Rectangle {
// 相加时,将两个矩形的宽度和高度相加
return Rectangle {
width: self.width + other.width,
height: self.height + other.height,
};
}
}
fn main() {
// Rust 中数值相关数据类型都实现了运算符相关 trait,可以直接使用 + 运算符相加
let a = 100_u32;
let b = 200_u32;
let sum = a + b;
println!("u32 相加后:{}", sum);
// 使用 + 运算符相加两个矩形
let rect1 = Rectangle { width: 10, height: 20 };
let rect2 = Rectangle { width: 5, height: 15 };
let rect3 = rect1 + rect2;
println!("矩形相加后:宽={}, 高={}", rect3.width, rect3.height);
}
shell> cargo run
u32 相加后:300
矩形相加后:宽=15, 高=35
9.3 打印调试(Debug 和 Display trait)
Debug trait 定义了调试输出的标准接口,数据类型需实现其 fmt() 函数,将内部数据转换为便于阅读的文本格式,以便开发调试。在 println! 宏中使用 {:?} 或 {:#?} 占位符时,编译器会自动调用 fmt() 函数以输出调试信息。下述示例,展示了如何为矩形结构体手动实现 Debug trait。
use std::fmt::{Debug, Formatter, Result};
// 定义一个矩形结构体
struct Rectangle {
width: u32,
height: u32,
}
// 为 Rectangle 类型实现 fmt::Debug trait
impl Debug for Rectangle {
fn fmt(&self, f: &mut Formatter) -> Result {
// 检查是否使用多行模式 :#? 标志
if f.alternate() {
// 自定义输出格式,带换行
return write!(f, "width: {} \nheight: {}", self.width, self.height);
}
// 自定义输出格式,不带换行
return write!(f, "width: {}, height: {}", self.width, self.height);
}
}
fn main() {
// 创建一个 Rectangle 类型的实例
let rect = Rectangle { width: 30, height: 40 };
// 使用 `{:?}` 占位符输出格式化的调试信息
println!("{:?}", rect);
// 使用 `{:#?}` 占位符输出格式化的调试信息(多行格式)
println!("{:#?}", rect);
// 使用 dbg! 宏打印指定变量的值并返回,打印效果等同于 `{:#?}`,但还会输出变量名和源代码中的行号
dbg!(rect);
}
shell> cargo run
width: 30, height: 40
width: 30
height: 40
[src\main.rs:35:5] rect = width: 30 height: 40
另外,std::fmt::Formatter 提供了一系列以 debug_ 开头的辅助函数(如 debug_struct 等),帮助开发者以更清晰和结构化的方式打印数据。这些函数会自动处理 {:?} 或 {:#?} 占位符,并根据需要自动进行缩进和换行。
use std::fmt::{Debug, Formatter, Result};
// 定义一个矩形结构体
struct Rectangle {
width: u32,
height: u32,
}
// 为 Rectangle 类型实现 fmt::Debug trait
impl Debug for Rectangle {
fn fmt(&self, f: &mut Formatter) -> Result {
// 自定义输出格式
return f.debug_struct("Rectangle")
.field("width", &self.width)
.field("height", &self.height)
.finish();
}
}
fn main() {
// 创建一个 Rectangle 类型的实例
let rect = Rectangle { width: 30, height: 40 };
// 使用 `{:?}` 占位符输出格式化的调试信息
println!("{:?}", rect);
// 使用 `{:#?}` 占位符输出格式化的调试信息(多行格式)
println!("{:#?}", rect);
// 使用 dbg! 宏打印指定变量的值并返回,打印效果等同于 `{:#?}`,但还会输出变量名和源代码中的行号
dbg!(rect);
}
shell> cargo run
Rectangle { width: 30, height: 40 }
Rectangle {
width: 30,
height: 40,
}
[src\main.rs:31:5] rect = Rectangle {
width: 30,
height: 40,
}
此外,Rust 提供了 #[derive] 属性宏,能够为结构体或枚举自动派生多个内置 trait 的实现。这免去了手动编写模板代码的繁琐,极大减少了重复性的样板代码。
use std::fmt::Debug;
#[allow(dead_code)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
// 创建一个 Rectangle 类型的实例
let rect = Rectangle { width: 30, height: 40 };
// 打印输出
println!("{:?}", rect);
println!("{:#?}", rect);
dbg!(rect);
}
shell> cargo run
Rectangle { width: 30, height: 40 }
Rectangle {
width: 30,
height: 40,
}
[src\main.rs:17:5] rect = Rectangle {
width: 30,
height: 40,
}
Display trait 定义了面向用户的文本输出标准接口,目标类型通过实现其 fmt() 函数,将自身转换为更加简洁、易读的文本表示。在 println! 宏中使用 {} 占位符时,编译器会自动调用 fmt() 方法输出格式化结果。由于用户展示内容通常依赖具体业务场景,因此 Display 无法通过 #[derive] 自动派生实现,需要开发者根据实际需求手动定义输出格式。
use std::fmt::{Display, Formatter, Result};
// 定义矩形结构体
struct Rectangle {
width: u32,
height: u32,
}
// 为 Rectangle 类型实现 fmt::Display trait
impl Display for Rectangle {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "矩形大小:宽={},高={}", self.width, self.height)
}
}
fn main() {
let rect = Rectangle { width: 30, height: 40 };
println!("{}", rect);
}
shell> cargo run
矩形大小:宽=30,高=40
9.4 默认值(Default trait)
Default trait 定义了默认值生成的标准接口,数据类型需实现其 default() 函数,返回一个预设的默认实例。Rust 中常用数据类型基本都已实现该 trait,对于自定义结构体,如果所有字段的类型均实现了 Default trait,则可以通过 #[derive(Default)] 自动派生实现。如果需要定义特定的默认值或包含额外初始化逻辑,则需要手动实现 default() 函数。下述示例中,展示了如何为矩形结构体手动实现 Default trait,并将宽度和高度的默认值都设置为 100。
#[allow(dead_code)]
#[derive(Debug)]
// #[derive(Debug, Default)]
struct Rectangle {
width: u32,
height: u32,
}
impl Default for Rectangle {
// 矩形默认宽高为 100
fn default() -> Self {
return Self {
width: 100,
height: 100,
};
}
}
fn main() {
let default_rectangle = Rectangle::default();
println!("{:?}", default_rectangle);
}
shell> cargo run
Rectangle { width: 100, height: 100 }
9.5 类型转换(From 和 Into trait)
From 和 Into trait 定义了类型转换的标准接口,用于描述不同类型之间的转换关系。其中,From trait 用于定义从其他类型转换为当前类型的规则;当目标类型实现了 From 后,Rust 会自动为源类型提供对应的 Into 实现。因此,通常只需实现 From,即可同时获得 Into 的转换能力。下述示例中,通过 From 和 Into trait,实现英寸到厘米的类型转换。
// 定义厘米结构体
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Centimeter(f64);
// 定义英寸结构体
#[derive(Debug, Clone)]
struct Inch(f64);
// 实现英寸转厘米,1 英寸 = 2.54 厘米
impl From<Inch> for Centimeter {
fn from(inch: Inch) -> Self {
return Centimeter(inch.0 * 2.54);
}
}
fn main() {
// 创建一个英寸实例
let inch = Inch(10.0);
// 使用 From trait 将英寸转换为厘米
let centimeter: Centimeter = Centimeter::from(inch.clone());
println!("from: {:?}", centimeter);
// 使用 Into trait 将英寸转换为厘米
let centimeter: Centimeter = inch.into();
println!("into: {:?}", centimeter);
}
shell> cargo run
from: Centimeter(25.4)
into: Centimeter(25.4)
From 和 Into 都用于实现类型转换,但在函数参数或泛型定义中,Into 更为灵活:From 通常要求明确指定具体类型,而 Into 则允许接收任何实现了相应转换的类型。
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Centimeter(f64);
#[derive(Debug, Clone)]
struct Inch(f64);
impl From<Inch> for Centimeter {
fn from(inch: Inch) -> Self {
return Centimeter(inch.0 * 2.54);
}
}
// 使用 Into trait 进行转换的函数,可以接收任意实现 Into<Centimeter> 的类型
fn convert_to_centimeter(value: impl Into<Centimeter>) -> Centimeter {
return value.into();
}
fn main() {
let inch = Inch(10.0);
let cm = convert_to_centimeter(inch);
println!("cm = {:?}", cm);
}
shell> cargo run
cm = Centimeter(25.4)
如果转换过程可能失败,应优先选择 TryFrom 与 TryInto。与 From/Into 不同,它们返回 Result 类型(见错误处理章节),能够强制开发者处理潜在的错误,确保程序的健壮性。
// 定义厘米结构体
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Centimeter(f64);
// 定义英寸结构体
#[derive(Debug, Clone)]
struct Inch(f64);
// 实现 TryFrom 来进行英寸到厘米的转换
use std::convert::TryFrom;
impl TryFrom<Inch> for Centimeter {
type Error = &'static str; // 定义错误类型
fn try_from(inch: Inch) -> Result<Self, Self::Error> {
if inch.0 < 0.0 {
Err("Negative inches cannot be converted to centimeters")
} else {
Ok(Centimeter(inch.0 * 2.54))
}
}
}
fn main() {
// 创建一个英寸实例
let inch = Inch(10.0);
// 使用 TryFrom trait 将英寸转换为厘米
match Centimeter::try_from(inch.clone()) {
Ok(centimeter) => println!("try_from: {:?}", centimeter),
Err(e) => println!("Error: {}", e),
}
// 使用 TryInto trait 将英寸转换为厘米
let result: Result<Centimeter, _> = inch.try_into();
match result {
Ok(centimeter) => println!("try_into: {:?}", centimeter),
Err(e) => println!("Error: {}", e),
}
}
shell> cargo run
try_from: Centimeter(25.4)
try_into: Centimeter(25.4)
9.6 数据比较(PartialEq 和 Eq)
PartialEq trait 定义了数据比较接口,用于判断两个值是否相等,数据类型需要实现其 eq() 函数,才能使用 == 和 != 运算符进行比较。Rust 内置的常用数据类型基本上已经实现了该 trait,对于自定义结构体,若其所有字段均已实现 PartialEq,则可通过 #[derive(PartialEq)] 快速派生;若需自定义判定逻辑(如忽略特定字段或处理特殊的相等规则),则需手动实现 eq() 方法。下述示例展示了如何为矩形结构体实现 PartialEq,使得两个矩形对象可以通过 == 和 != 运算符进行比较。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// 实现 PartialEq trait,用于判断矩形是否相等
impl PartialEq for Rectangle {
fn eq(&self, other: &Self) -> bool {
// 比较矩形的宽度和高度是否相等
return self.width == other.width && self.height == other.height;
}
}
fn main() {
let rect1 = Rectangle { width: 100, height: 200 };
let rect2 = Rectangle { width: 100, height: 200 };
let rect3 = Rectangle { width: 200, height: 300 };
// 使用 == 和 != 运算符比较矩形
println!("rect1 == rect2: {}", rect1 == rect2); // 输出 true
println!("rect1 == rect2: {}", rect1 != rect2); // 输出 false
println!("rect1 == rect3: {}", rect1 == rect3); // 输出 false
}
此外,PartialEq trait 还定义了用于判定不相等的 ne() 函数。由于该函数默认实现为 eq() 的取反,因此开发者通常只需实现 eq() 即可直接支持 != 运算符。如果有特殊需求,亦可手动重写 ne() 函数以定制特定的不相等判定逻辑。
#![allow(unused)]
fn main() {
impl PartialEq for Rectangle {
fn eq(&self, other: &Self) -> bool {
return self.width == other.width && self.height == other.height;
}
// 实现 PartialEq 特征的 ne 方法, 默认已实现为 eq 函数取反
fn ne(&self, other: &Self) -> bool {
return !self.eq(other);
}
}
}
Eq trait 继承自 PartialEq,是一个不包含任何函数的标记 trait,用于标记类型满足完全相等的要求,并遵循自反性原则,即 x == x 始终为 true。例如,Rust 的浮点数遵循 IEEE 规范,对于未定义的计算返回 NaN(Not a Number)。由于不同场景下 NaN 的含义不同,两个 NaN 比较不保证相等,因此不满足自反性。正因如此,Rust 的浮点数没有实现 Eq。依赖浮点数的相等判断可能导致意外结果,建议使用 is_nan() 方法显式检测 NaN,或在需要精确比较时考虑其他替代方案。下述示例展示了如何使用 is_nan() 显式检测 NaN,并解释了两个 NaN 值之间的比较结果:
fn main() {
// 除零错误,结果为 NaN
let x: f32 = 0.0 / 0.0;
// 无效的平方根计算,结果也为 NaN
let y: f32 = (-1.0_f32).sqrt();
// 尝试比较两个 NaN 值,结果为 false
match x == y {
true => println!("x 和 y 是相等的"),
false => println!("x 和 y 是不相等的"),
}
// 显式检测 x 和 y 是否为 NaN
if x.is_nan() && y.is_nan() {
println!("x 和 y 都是 NaN");
}
}
shell> cargo run
x 和 y 是不相等的
x 和 y 都是 NaN
9.7 数据克隆 Clone
Clone trait 定义了对象克隆的标准接口,数据类型需实现其 clone() 函数,在该函数中实现复制逻辑,不同类型的实现方式可能有所不同,常见的包括深度复制、浅复制以及增加引用计数等方式。下述示例展示了如何为自定义结构体 Rectangle 手动实现 Clone trait。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Clone for Rectangle {
fn clone(&self) -> Self {
// 深度复制,创建一份全新的副本,不共享任何数据
return Rectangle {
width: self.width,
height: self.height,
};
}
}
fn main() {
let rect1 = Rectangle { width: 100, height: 200 };
// 深度复制,修改副本的值,不影响原对象
let mut rect2 = rect1.clone();
rect2.width = 200;
println!("{:?}", rect1);
println!("{:?}", rect2);
}
shell> cargo run
Rectangle { width: 100, height: 200 }
Rectangle { width: 200, height: 200 }
9.8 所有权与 Copy
在 Rust 中,每个变量都有一个所有者(Owner),且任意时刻只能有一个所有者,当所有者离开作用域,自动释放资源。当将一个变量赋值给另一个变量、作为函数参数传递或者从函数返回时(闭包捕获),所有权会从原变量转移到新变量,原变量随即失效,不再可用。
fn main() {
// 创建一个字符串变量,所有权归 str1
let str1 = String::from("你好");
// 在赋值时,所有权转移给 str2,原变量 str1 失效,无法继续使用
let str2 = str1;
// 由于所有权已转移给 str2,下面的代码会报错
// println!("{}", str1); // 无法继续使用,编译时会报错
// 现在可以安全地使用 str2
println!("{}", str2); // 输出 "你好"
}
如果数据类型实现了 Copy trait,那么在赋值、函数参数传递或返回值(闭包捕获)时,数据会按位复制,而不是转移所有权,原变量仍然有效,可以继续使用。Copy trait 是一个不包含任何函数的标记 trait,用于告诉编译器在这些操作中执行按位复制而非所有权转移。Copy trait 继承自 Clone,要实现 Copy,类型必须实现 Clone,且其所有字段也都实现 Copy,否则编译器无法自动执行按位复制。
在 Rust 中,占用空间较小且大小固定的基本类型(如整数、浮点数、布尔值、字符等)默认实现已 Copy trait,而占用内存较大的类型(如 String)则通常没有实现 Copy trait。
// 实现 Clone 和 Copy trait
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct Rectangle {
width: u32,
height: u32,
}
// 仅实现 Clone trait
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Circle {
radius: u32,
}
fn main() {
// 矩形实现了 Copy trait,赋值时会自动复制,赋值后原变量 rect1 可以继续使用
let rect1 = Rectangle { width: 100, height: 200 };
let rect2 = rect1; // 赋值时会自动复制
println!("rect1: {:?}", rect1); // rect1 可以继续使用
println!("rect2: {:?}", rect2);
// 圆形未实现 Copy trait,赋值会转移数据所有权, 赋值后 circle1 不能继续使用
let circle1 = Circle { radius: 50 };
let circle2 = circle1; // 赋值时不会复制
// println!("circle1: {:?}", circle1); // circle1 不能继续使用
println!("circle2: {:?}", circle2);
// 基本类型默认已实现 Copy trait
let num1 = 100_i32;
let num2 = num1; // 赋值时会自动复制
println!("num1={num1}, num2={num2}");
}
对于数组类型,如果数组元素已经实现了 Copy trait,那么数组本身也会自动实现 Copy。在赋值、函数参数传递或返回值时,需要特别注意复制操作带来的性能开销。
use std::time::Instant;
fn main() {
let array = [1_u8; 512 * 1024];
let start = Instant::now();
// 由于数组元素实现了 Copy trait,数组赋值时会复制整个数据
let other = array;
println!("数组复制耗时: {:?}", start.elapsed());
println!("len = {}", other.len());
}
shell> cargo run
数组复制耗时: 24.2µs
len = 524288
9.9 引用转换 AsRef
AsRef trait 定义了将一种类型转换为另一种类型的不可变引用的标准接口,数据类型需实现其 as_ref() 函数将自身转换为目标类型的引用。由于该转换仅创建一个借用关系,不会发生数据复制,因此通常用于函数参数泛化,使函数能够同时接收多种具有相同引用表示的类型。
例如,String、&str 和 str 都可以表示字符串数据,但它们的类型不同。如果函数仅接收 &str 类型参数,则调用者传入 String 时需要手动获取字符串切片;而通过 AsRef 泛化参数后,函数可以同时接收多种字符串类型,并统一通过 as_ref() 获取 &str 引用。
fn print_text<T: AsRef<str>>(text: T) {
let text: &str = text.as_ref();
println!("{}", text);
}
fn main() {
// String 和 str 都实现了 AsRef<str> 的 as_ref() 函数,并返回其 &str
let s1: &str = "hello";
let s2: String = String::from("world");
let s3: Box<str> = "rust".into();
print_text(s1); // &str
print_text(s2); // String
print_text(s3); // Box<str>
}
shell> cargo run
hello
world
rust
下述代码中,使用 UserId 和 OrderId 包装字符串 ID,在类型层面区分不同业务含义的标识,避免将本质相同但语义不同的数据混用。通过为两者都实现 AsRef trait,使它们都能够以统一的 &str 引用形式访问内部数据,从而既保留了类型安全性,又允许通用函数对不同类型的 ID 进行统一处理。
struct UserId(String);
struct OrderId(String);
impl AsRef<str> for UserId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for OrderId {
fn as_ref(&self) -> &str {
&self.0
}
}
fn print_id<T: AsRef<str>>(id: T) {
println!("ID: {}", id.as_ref());
}
fn main() {
let user_id = UserId("100".to_string());
let order_id = OrderId("200".to_string());
print_id(user_id);
print_id(order_id);
}
shell> cargo run
ID: 100
ID: 200
9.10 资源释放(Drop trait)
Drop trait 定义了资源释放的标准接口,数据类型通过实现其 drop() 函数来定义资源清理逻辑,例如关闭文件、断开网络连接、释放内存或回收其他外部资源。当变量离开作用域时,Rust 会自动调用该类型实现的 drop() 方法,以完成资源释放。此外,也可以通过调用标准库提供的 std::mem::drop() 函数,提前主动触发资源清理。
#[allow(dead_code)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Drop for Rectangle {
fn drop(&mut self) {
println!("执行清理操作: {:?}", self);
}
}
fn main() {
{
// 离开作用域时,会自动调用 drop 函数释放资源
let _rect = Rectangle { width: 100, height: 200 };
}
{
let rect = Rectangle { width: 100, height: 200 };
drop(rect); // 手动调用 drop 函数释放资源
println!("rect 已被手动释放");
}
}
#![allow(unused)]
fn main() {
shell> cargo run
执行清理操作: Rectangle { width: 100, height: 200 }
执行清理操作: Rectangle { width: 100, height: 200 }
rect 已被手动释放
}
9.11 解引用(Deref 和 DerefMut trait)
Deref trait 定义了解引用操作的标准接口,数据类型通过实现其 deref 函数来自定义解引用操作符 * 的行为。例如,智能指针类型可以通过 Deref 将自身转换为内部数据的引用,使解引用操作能够访问实际存储的数据。Deref 和 DerefMut trait 的主要区别在于解引用后是否允许修改数据:Deref 用于实现不可变解引用,只能通过共享引用访问数据;DerefMut 用于实现可变解引用,允许通过可变引用修改数据。由于 DerefMut trait 继承自 Deref trait,因此实现 DerefMut 时必须同时实现 Deref。
use std::ops::DerefMut;
use std::ops::Deref;
#[derive(Debug)]
struct IntWrapper {
value: i32,
}
impl Deref for IntWrapper {
type Target = i32;
// 实现自定义解引用行为,解引用将 IntWrapper 转为 i32
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl DerefMut for IntWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl IntWrapper {
// 扩展功能...
}
fn main() {
let mut number = IntWrapper { value: 100 };
// 使用解引用来访问 IntWrapper 内部的值并修改它
*number += 100;
println!("{:?}", number);
// 使用解引用来访问 IntWrapper 内部的值
let value = *number;
println!("value={value}");
}
shell> cargo run
IntWrapper { value: 200 }
value=200
9.12 闭包(Fn、FnMut 和 FnOnce)
在 Rust 中,闭包(Closure)是一种匿名函数(没有名字的函数),既能像普通函数一样接收参数并返回结果,还可以赋值给变量、作为函数参数传递或作为函数返回值,也可以定义在函数内部,用于封装局部逻辑。闭包的语法形式为 |参数列表| 表达式 或 |参数列表| { 代码块 }。
fn main() {
// 1. 表达式形式:|参数| 表达式,可以像普通函数一样调用
let add_func = |a: i32, b: i32| a + b;
let sum = add_func(1, 2);
println!("sum = {sum}");
// 2. 表达式形式:|参数| { 代码块 },参数类型可以省略,编译器通常可以自动推导出来
let circle_area_func = |radius| {
let area = 3.14 * radius * radius;
area // 闭包的最后一行默认就是返回值,不需要显式使用 `return` 关键字
};
// 使用闭包实现函数内部逻辑复用
let area1 = circle_area_func(2.0);
let area2 = circle_area_func(3.5);
println!("area1 = {area1}, area2 = {area2}");
}
shell> cargo run
sum = 3
area1 = 12.56, area2 = 38.465
此外,闭包还能自动捕获并使用其所在作用域中的外部变量,而无需显式地通过参数传递。
fn main() {
let a = 100;
let b = 200;
// 闭包可以捕获所在作用域中的外部变量(a 和 b)
let sum_func = || a + b;
let res = sum_func();
println!("{res}");
}
shell> cargo run
300
闭包本质是编译器在编译时生成的一个临时结构体,捕获的外部变量存放在该结构体中。根据闭包捕获外部变量的方式不同,编译器会自动为其实现 Fn、FnMut 或 FnOnce trait。
| 特征 | 说明 |
|---|---|
| Fn | 通过不可变引用的方式捕获外部变量,在闭包中只读取,不能修改。 |
| FnMut | 通过可变借用的方式捕获外部变量,在闭包中可以修改。 |
| FnOnce | 通过获取外部变量所有权的方式捕获外部变量,因此闭包只能调用一次。 |
fn main() {
// 1. Fn:闭包只读访问外部变量,常见于计算或查询
let radius = 2.0;
let fn_func = || 3.14 * radius * radius; // 闭包只读访问 radius
println!("Fn: {}", fn_func());
println!("Fn: {}", fn_func()); // 可以多次调用
// Fn 通过不可变引用的方式捕获一个外部变量,所以其大小等于一个引用的大小 8 字节(64位平台)
println!("fn_func 大小: {}", size_of_val(&fn_func));
// 2. FnMut:闭包修改外部变量,常见于计数器或状态更新
let mut counter = 0;
let mut mut_func = || {
counter += 1;
println!("FnMut: counter={counter}");
};
mut_func();
mut_func(); // 可以多次调用
// 3. FnOnce:闭包会获取外部变量所有权,常见于所有权转移或资源释放
let hello = String::from("hello");
let once_func = || {
// String 类型没有实现 Copy trait,赋值时所有权会转移
let text = hello; // drop(hello) 释放资源也会捕获外部所有权
println!("FnOnce: {text}");
};
// 通过获取外部变量所有权捕获 String,因此闭包大小为 String 的大小,约 24 字节(64 位平台)。
println!("once_func 大小: {}", size_of_val(&once_func));
// 只能调用一次,第二次调用会报错,因为第一次调用 hello 所有权被转移了
once_func();
// once_funce(); // 编译报错
}
shell> cargo run
Fn: 12.56
Fn: 12.56
fn_func 大小: 8
FnMut: counter=1
FnMut: counter=2
once_func 大小: 24
FnOnce: hello
Fn、FnMut 和 FnOnce 三者之间存在层次化的继承关系:Fn 继承 FnMut,而 FnMut 又继承 FnOnce(即 FnOnce → FnMut → Fn)。由于子 trait 必须同时满足父 trait 的约束,因此,实现了 Fn 的闭包可以当作 FnMut 或 FnOnce 使用,而实现了 FnMut 的闭包同样可以当作 FnOnce 使用。这种设计让闭包能够灵活适应不同的调用场景,从只读访问,到可变访问,再到一次性消费外部变量。
// 接收 Fn
fn call_fn(func: impl Fn()) {
println!("call_fn:");
func();
func();
}
// 接收 FnMut
fn call_mut<F: FnMut()>(mut func: F) {
println!("call_mut:");
func();
func();
}
// 接收 FnOnce
fn call_once(func: impl FnOnce()) {
println!("call_once:");
func();
}
fn main() {
let x = 42;
// 只读捕获 ⇒ Fn,同时也是 FnMut、FnOnce
let closure = || println!("x = {}", x);
// 可以传给 Fn
call_fn(closure);
// 也可以传给 FnMut
call_mut(closure);
// 也可以传给 FnOnce
call_once(closure);
}
shell> cargo run
call_fn:
x = 42
x = 42
call_mut:
x = 42
x = 42
call_once:
x = 42
9.13 迭代器 Iterator trait
Iterator trait 定义了数据遍历的函数接口,提供一种统一的方式来访问数据集合中的各个元素,无需暴露该数据集合的内部实现细节。实现该 trait 的类型需要实现其 next() 函数,返回集合中的下一个元素。下述示例,展示了如何通过迭代器遍历一个自定义结构体。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 创建迭代器
fn into_iter(self) -> IntoIter {
return IntoIter { rect: self, index: 0 };
}
}
// 定义一个迭代器,专门用于遍历 Rectangle
struct IntoIter {
rect: Rectangle,
index: u32,
}
// 实现 Iterator trait,并在 next() 函数中实现遍历结构体所有字段
impl Iterator for IntoIter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
let value = match self.index {
0 => Some(self.rect.width), // 返回第一个字段 width
1 => Some(self.rect.height), // 返回第二个字段 height
_ => None, // 遍历完成,返回 None
};
self.index += 1; // 增加索引,指向下一个字段
return value;
}
}
fn main() {
// 手动调用 next() 函数遍历
let rect1 = Rectangle { width: 100, height: 200 };
let mut into_iter = rect1.into_iter();
loop {
match into_iter.next() {
Some(value) => println!("rect1 手动调用:{value}"),
None => break,
}
}
// for 循环支持迭代器遍历
let rect2 = Rectangle { width: 100, height: 200 };
for value in rect2.into_iter() {
println!("rect2 自动调用:{value}");
}
}
shell> cargo run
rect1 手动调用:100
rect1 手动调用:200
rect2 自动调用:100
rect2 自动调用:200
根据迭代器关联数据的方式不同,可以实现三种不同的迭代器:不可变引用、可变借用以及获取数据所有权。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn iter(&self) -> Iter<'_> {
return Iter { rect: self, index: 0 };
}
fn iter_mut(&mut self) -> IterMut<'_> {
return IterMut { rect: self, index: 0 };
}
fn into_iter(self) -> IntoIter {
return IntoIter { rect: self, index: 0 };
}
}
// 1. 不可变引用迭代器
struct Iter<'a> {
rect: &'a Rectangle, // 通过不可变引用关联
index: u32,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a u32;
fn next(&mut self) -> Option<Self::Item> {
let value = match self.index {
0 => Some(&self.rect.width),
1 => Some(&self.rect.height),
_ => None,
};
self.index += 1;
value
}
}
// 2. 可变借用迭代器
struct IterMut<'a> {
rect: &'a mut Rectangle, // 通过可变借用关联
index: u32,
}
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut u32;
fn next(&mut self) -> Option<Self::Item> {
// 使用原始指针避开多次借用检查,需要自己保证安全性,仅用于演示
let rect = unsafe { &mut *(self.rect as *mut Rectangle) };
// let rect = &mut self.rect;
let value = match self.index {
0 => Some(&mut rect.width),
1 => Some(&mut rect.height),
_ => None,
};
self.index += 1;
return value;
}
}
// 3. 所有权转移迭代器
struct IntoIter {
rect: Rectangle, // 直接持有数据,拥有数据的所有权
index: u32,
}
impl Iterator for IntoIter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
let value = match self.index {
0 => Some(self.rect.width),
1 => Some(self.rect.height),
_ => None,
};
self.index += 1;
value
}
}
fn main() {
let mut rect = Rectangle { width: 100, height: 200 };
for value in rect.iter() {
println!("不可变引用迭代: {value}");
}
// 遍历时可以通过 enumerate 获取元素的索引和值
for (index, value) in rect.iter().enumerate() {
println!("不可变引用迭代: index={index}, value={value}");
}
for value in rect.iter_mut() {
*value += 100;
println!("可变借用迭代: {value}");
}
for value in rect.into_iter() {
println!("所有权转移迭代: {value}");
}
// 所有权被转移,原变量 rect 无法使用
// println!("rect: {:?}", rect);
}
shell> cargo run
可变引用迭代: 100
不可变引用迭代: 200
不可变引用迭代: index=0, value=100
不可变引用迭代: index=1, value=200
可变借用迭代: 200
可变借用迭代: 300
所有权转移迭代: 200
所有权转移迭代: 300
另外,通过为类型实现 IntoIterator trait,可以使其与 Rust 的 for 循环生态无缝集成。当 for 循环作用于该类型时,编译器会自动调用其 into_iter 方法,将其转换为对应的迭代器,实现更简洁直观的遍历操作。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn iter(&self) -> Iter<'_> {
return Iter { rect: self, index: 0 };
}
fn iter_mut(&mut self) -> IterMut<'_> {
return IterMut { rect: self, index: 0 };
}
fn into_iter(self) -> IntoIter {
return IntoIter { rect: self, index: 0 };
}
}
// 1. 不可变引用迭代器
struct Iter<'a> {
rect: &'a Rectangle, // 通过不可变引用关联
index: u32,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a u32;
fn next(&mut self) -> Option<Self::Item> {
let value = match self.index {
0 => Some(&self.rect.width),
1 => Some(&self.rect.height),
_ => None,
};
self.index += 1;
value
}
}
// 2. 可变借用迭代器
struct IterMut<'a> {
rect: &'a mut Rectangle, // 通过可变借用关联
index: u32,
}
impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut u32;
fn next(&mut self) -> Option<Self::Item> {
// 使用原始指针避开多次借用检查,需要自己保证安全性,仅用于演示
let rect = unsafe { &mut *(self.rect as *mut Rectangle) };
// let rect = &mut self.rect;
let value = match self.index {
0 => Some(&mut rect.width),
1 => Some(&mut rect.height),
_ => None,
};
self.index += 1;
return value;
}
}
// 3. 所有权转移迭代器
struct IntoIter {
rect: Rectangle, // 直接持有数据,拥有数据的所有权
index: u32,
}
impl Iterator for IntoIter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
let value = match self.index {
0 => Some(self.rect.width),
1 => Some(self.rect.height),
_ => None,
};
self.index += 1;
value
}
}
// 为 &Rectangle 实现 IntoIterator
impl<'a> IntoIterator for &'a Rectangle {
type Item = &'a u32; // 返回的是字段的引用
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
Iter {
rect: self,
index: 0,
}
}
}
// 为 &mut Rectangle 实现 IntoIterator
impl<'a> IntoIterator for &'a mut Rectangle {
type Item = &'a mut u32; // 返回的是字段的可变引用
type IntoIter = IterMut<'a>;
fn into_iter(self) -> Self::IntoIter {
IterMut {
rect: self,
index: 0,
}
}
}
// 为 Rectangle 实现 IntoIterator
impl IntoIterator for Rectangle {
type Item = u32; // 获取所有权
type IntoIter = IntoIter;
fn into_iter(self) -> Self::IntoIter {
IntoIter {
rect: self,
index: 0,
}
}
}
fn main() {
let mut rect = Rectangle { width: 100, height: 200 };
for value in rect.iter() {
println!("不可变引用迭代: {value}");
}
// 遍历时可以通过 enumerate 获取元素的索引和值
for (index, value) in rect.iter().enumerate() {
println!("不可变引用迭代: index={index}, value={value}");
}
for value in rect.iter_mut() {
*value += 100;
println!("可变借用迭代: {value}");
}
for value in rect.into_iter() {
println!("所有权转移迭代: {value}");
}
// 所有权被转移,原变量 rect 无法使用
// println!("rect: {:?}", rect);
}
shell> cargo run
不可变引用迭代: 100
不可变引用迭代: 200
不可变引用迭代: index=0, value=100
不可变引用迭代: index=1, value=200
可变借用迭代: 200
可变借用迭代: 300
所有权转移迭代: 200
所有权转移迭代: 300
在 Rust 中,数组、切片以及常见的集合类型(详见下一章)默认支持三种不同的迭代方式,并实现了 IntoIterator trait。
iter():获取不可变引用迭代器,仅支持读取元素,无法修改元素;iter_mut():获取可变引用迭代器,允许在遍历时修改元素;into_iter():获取所有权迭代器,遍历过程中会获取元素的所有权,原始变量在之后无法再使用(对于数组和切片,是否可以再次使用取决于元素是否实现了Copytrait)。
fn main() {
let mut arr = [1, 2, 3];
// 不可变引用迭代器:iter()
for item in arr.iter() {
println!("iter: {}", item);
}
// 可变引用迭代器:iter_mut()
// 注:数组已实现 IntoIterator trait,&mut arr 等同于 arr.iter_mut()
for item in &mut arr {
*item *= 10; // 修改元素
println!("iter_mut: {}", item);
}
// 所有权迭代器:into_iter()
for item in arr.into_iter() {
println!("into_iter: {}", item);
}
// 调用 into_iter() 时,所有权会被转移,但如果数组或切片的
// 元素实现了 Copy trait,元素会被复制,原始变量仍然可以访问。
println!("{:?}", arr);
// 字符串没有实现 Copy trait,所有权被转移后,原数组无法继续访问
let texts = ["a".to_string(), "b".to_string(), "c".to_string()];
for text in texts.into_iter() {
println!("texts.into_iter: {}", text);
}
// println!("{:?}", texts); // 编译报错,原数组无法继续访问
}
shell> cargo run
iter: 1
iter: 2
iter: 3
iter_mut: 10
iter_mut: 20
iter_mut: 30
into_iter: 10
into_iter: 20
into_iter: 30
[10, 20, 30]
texts.into_iter: a
texts.into_iter: b
texts.into_iter: c
此外,迭代器还提供了一系列函数,如filter(过滤)、 for_each(遍历)、find(查找)和 map(类型转换)等几十种操作函数。这些函数不仅扩展了迭代器的功能,还支持组合使用,以实现更复杂的操作。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 5];
// filter: 过滤出大于 5 的数字
let filtered: Vec<_> = numbers.iter().filter(|&&x| x > 5).collect();
println!("过滤大于 5 的数字: {:?}", filtered);
// for_each: 遍历并打印每个数字
let mut string = String::from("遍历:");
numbers.iter().for_each(|&x|
string.push_str(&(x.to_string() + ", "))
);
println!("{string}");
// find: 查找第一个等于 5 的数字
let found = numbers.iter().find(|&&x| x == 5);
match found {
Some(&x) => println!("找到第一个 5: {}", x),
None => println!("没有找到 5"),
}
// map: 将每个数字转换为 f64 类型
let as_f64: Vec<_> = numbers.iter().map(|&x| x as f64).collect();
println!("将数字转换为 f64 类型: {:?}", as_f64);
// max: 最大值
let max = numbers.iter().max();
match max {
Some(&x) => println!("最大值: {}", x),
None => println!("没有最大值"),
}
// mix: 最小值
let min = numbers.iter().min();
match min {
Some(&x) => println!("最小值: {}", x),
None => println!("没有最小值"),
}
// skip 和 take: 跳过前 3 个数字,取接下来的 4 个数字
let skipped_and_taken: Vec<_> = numbers.iter().skip(3).take(4).collect();
println!("跳过前 3 个数字,取接下来的 4 个: {:?}", skipped_and_taken);
// 累加、累乘
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
println!("累加总和: {}", sum);
println!("累乘乘积: {}", product);
// 组合使用: 先过滤出大于 5 的数字,然后将它们平方,最后相加
let combined_sum: i32 = numbers.iter()
.filter(|&&x| x > 5)
.map(|&x| x * x)
.sum();
println!("大于 5 的数字的平方和: {}", combined_sum);
}
shell> cargo run
过滤大于 5 的数字: [6, 7, 8, 9, 10]
遍历:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 5,
找到第一个 5: 5
将数字转换为 f64 类型: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 5.0]
最大值: 10
最小值: 1
跳过前 3 个数字,取接下来的 4 个: [4, 5, 6, 7]
累加总和: 60
累乘乘积: 18144000
大于 5 的数字的平方和: 330