第四章:高级特性
本章将深入探讨使 Rust 成为一门富有表现力且功能强大的语言的高级特性。我们将涵盖泛型、trait、智能指针、闭包和迭代器。
示意图:智能指针生态与关系
graph LR
Box --> Heap[堆分配]
Rc --> Owners[多所有者]
Arc --> ThreadSafe[线程安全多所有者]
RefCell --> BorrowCheck[运行时借用检查]
Cell --> CellSet[按值 set]
Rc --> RefCell
Arc --> Mutex
Mutex --> Exclusive[互斥访问]
RwLock --> RW[多读单写]
56. 什么是泛型 (Generics)?
答: 泛型是一种编程语言特性,它允许我们在定义函数、结构体、枚举等时不指定具体的类型,而是使用一个抽象的“类型参数”。这使得我们可以编写更灵活、可重用且不会牺牲类型安全的代码。
// 泛型函数:T 可以是任何类型
fn identity<T>(item: T) -> T {
item
}
// 泛型结构体:可以持有任何类型的 x 和 y
struct Point<T> {
x: T,
y: T,
}
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
进阶示例:trait bound、where 子句与 const generics
// 使用 trait bound 约束 T 必须可比较和可显示
fn max_display<T>(a: T, b: T) -> T
where
T: std::cmp::Ord + std::fmt::Display,
{
let m = if a >= b { a } else { b };
println!("max = {}", m);
m
}
// const generics:在类型层面携带常量参数
struct FixedVec<T, const N: usize> {
data: [T; N],
}
impl<T: Default + Copy, const N: usize> FixedVec<T, N> {
fn new() -> Self { Self { data: [T::default(); N] } }
}
57. 什么是 Trait?它和接口 (Interface) 有什么关系?
答: Trait 用于告诉 Rust 编译器某种类型具有哪些并且可以与其它类型共享的功能。它类似于其他语言中的接口(Interface),用于定义共享的行为。
pub trait Summary {
fn summarize_author(&self) -> String;
// 带有默认实现的方法
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Trait for Type
的语法来实现一个 trait,从而保证该类型拥有 trait 中定义的所有方法。
进阶示例:为多种类型实现 trait,重写默认方法
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String { format!("(more from {})", self.summarize_author()) }
}
pub struct Article { pub author: String, pub title: String }
pub struct Tweet { pub user: String, pub content: String }
impl Summary for Article {
fn summarize_author(&self) -> String { self.author.clone() }
fn summarize(&self) -> String { format!("{} - {}", self.title, self.author) }
}
impl Summary for Tweet {
fn summarize_author(&self) -> String { self.user.clone() }
}
58. 如何使用 Trait 作为函数参数?
答:
你可以使用 impl Trait
语法或“trait bound”语法,来接受任何实现了特定 trait 的类型作为参数。
// 使用 impl Trait (更简洁)
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 使用 trait bound (更通用,适用于复杂情况)
pub fn notify_long<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Summary
trait。
59. 如何返回实现了 Trait 的类型?
答:
你也可以在返回值位置使用 impl Trait
,来返回一个实现了某个 trait 的具体类型,而无需写出该类型的确切名称。
这对于返回闭包或迭代器这类类型特别有用,因为它们的具体类型可能非常复杂,甚至无法写出。
fn returns_summarizable() -> impl Summary {
Tweet { // Tweet 是一个实现了 Summary 的结构体
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
进阶示例:返回闭包与迭代器
// 返回闭包(使用 impl Trait 隐藏复杂返回类型)
fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y }
// 返回迭代器(链式适配器)
fn evens_up_to(n: u32) -> impl Iterator<Item = u32> {
(0..=n).filter(|v| v % 2 == 0)
}
60. 什么是智能指针 (Smart Pointers)?
答:
智能指针是一类数据结构,它们的行为类似于指针,但还拥有额外的元数据和能力。它们通常通过实现 Deref
和 Drop
trait 来实现这些功能。
Deref
trait 允许智能指针实例的行为像引用一样,这样你就可以编写既适用于智能指针也适用于普通引用的代码。Drop
trait 允许你自定义当智能指针实例离开作用域时运行的代码,通常用于释放资源。
String
和 Vec<T>
实际上就是智能指针。
61. Box<T>
是什么?它有什么用?
答:
Box<T>
是最简单的智能指针,它允许你将数据存储在堆上而不是栈上,而指针本身留在栈上。
主要用途: 1. 当有一个在编译时无法确定大小的类型,而又想在需要确切大小的上下文中使用它时(例如递归类型)。 2. 当你拥有大量数据并希望转移所有权,但又不希望在移动时复制所有数据时。 3. 当你希望拥有一个值,只关心它是否实现了某个特定的 trait,而不是它的具体类型时(trait object)。
// 递归类型示例:Cons List
enum List {
Cons(i32, Box<List>),
Nil,
}
进阶示例:Box
与 trait object
trait Draw { fn draw(&self); }
impl Draw for String { fn draw(&self) { println!("label: {}", self); } }
fn draw_widget(widget: Box<dyn Draw>) { widget.draw(); }
fn main() {
let w: Box<dyn Draw> = Box::new(String::from("OK"));
draw_widget(w);
}
62. Rc<T>
是什么?它和 Box<T>
有什么不同?
答:
Rc<T>
是引用计数 (Reference Counting) 智能指针。它允许多个所有者共同拥有同一份数据。
- 当克隆
Rc<T>
时,它不会深拷贝数据,而是增加一个引用计数。 - 当一个
Rc<T>
实例被 drop 时,引用计数减一。 - 只有当引用计数为零时,数据才会被清理。
Rc<T>
只能用于单线程场景。它与 Box<T>
的核心区别在于,Box<T>
强制执行 Rust 的单一所有权规则,而 Rc<T>
允许多重所有权。
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // 增加引用计数
let c = Rc::clone(&a); // 再次增加引用计数
进阶示例:Rc<RefCell<T>>
与 Weak
打破循环
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]) });
*leaf.parent.borrow_mut() = Rc::downgrade(&branch); // 使用 Weak 以避免循环引用
}
63. RefCell<T>
和内部可变性是什么?
答:
RefCell<T>
是一个在运行时而不是编译时强制执行借用规则的智能指针。通常情况下,你不能在拥有一个不可变引用的同时再获取一个可变引用。但 RefCell<T>
允许你这样做。
这种模式被称为内部可变性 (Interior Mutability),即一个表面上不可变的值,其内部的数据可以被修改。
borrow()
方法返回一个不可变引用Ref<T>
。borrow_mut()
方法返回一个可变引用RefMut<T>
。
如果在运行时违反了借用规则(例如,在已有一个可变借用的情况下再次请求可变借用),程序将会 panic
。
64. 为什么需要 Rc<RefCell<T>>
这种组合?
答:
Rc<T>
允许多个所有者,但是它是不可变的。你不能在拥有多个 Rc<T>
指针的情况下,获取一个可变引用来修改数据。
通过将 RefCell<T>
包装在 Rc<T>
内部,即 Rc<RefCell<T>>
,你可以实现拥有多个所有者,并且可以修改数据。
Rc<T>
负责允许多个所有者。RefCell<T>
负责提供内部可变性,并确保借用规则在运行时得到遵守。
这是在单线程环境中创建复杂数据结构(如图、树)时非常常见的模式。
65. 什么是闭包 (Closures)?
答: 闭包是一种可以捕获其环境的匿名函数。 - 匿名: 它们没有函数名。 - 捕获环境: 它们可以访问定义它们的作用域中的变量。
let x = 4;
let equal_to_x = |z| z == x; // 这个闭包捕获了 x
let y = 4;
assert!(equal_to_x(y));
进阶示例:Fn
/FnMut
/FnOnce
捕获差异
fn call_fn<F: Fn()>(f: F) { f(); }
fn call_fn_mut<F: FnMut()>(mut f: F) { f(); }
fn call_fn_once<F: FnOnce()>(f: F) { f(); }
fn main() {
let s = String::from("hello");
let f_once = || drop(s); // 获取所有权 -> FnOnce
call_fn_once(f_once);
let mut n = 0;
let mut f_mut = || { n += 1; }; // 可变借用 -> FnMut
call_fn_mut(f_mut);
let x = 1;
let f = || println!("{}", x); // 不可变借用 -> Fn
call_fn(f);
}
66. 闭包如何捕获环境?有哪几种方式?
答:
闭包可以通过三种方式捕获变量,这三种方式对应 Fn
、FnMut
和 FnOnce
三个 trait。
FnOnce
: 闭包会获取变量的所有权。Once
表示它至少需要获取所有权,因此只能被调用一次。FnMut
: 闭包会可变地借用变量。Mut
表示它可以改变环境中的变量。Fn
: 闭包会不可变地借用变量。
Rust 会根据闭包如何使用变量来自动推断它应该实现哪个 trait。
67. move
关键字在闭包中有什么用?
答:
在闭包前使用 move
关键字会强制闭包获取它所使用的所有环境变量的所有权。
这在将闭包传递给新线程时非常有用,可以确保闭包所引用的数据在新线程中是有效的。
use std::thread;
let v = vec![1, 2, 3];
// 如果没有 move,编译器会报错,因为它不知道 v 在新线程中能活多久
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
68. 什么是迭代器 (Iterators)?
答: 迭代器是一种允许你对一个项的序列(比如集合中的元素)进行遍历的模式。Rust 的迭代器是惰性的 (lazy),这意味着在你不主动消耗它之前,它不会产生任何效果。
所有迭代器都实现了 Iterator
trait,它只需要你实现一个 next
方法。
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter(); // iter() 创建一个迭代器
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
69. 消耗型适配器和迭代器适配器有什么区别?
答:
- 消耗型适配器 (Consuming Adaptors): 这类方法会调用 next
方法来消耗(使用掉)迭代器。例如 sum()
方法,它会遍历所有元素并计算总和,从而获得迭代器的所有权。collect()
是另一个例子。
- 迭代器适配器 (Iterator Adaptors): 这类方法会将一个迭代器转换成另一个迭代器。它们是惰性的,你需要链接一个消耗型适配器才能真正开始执行。map()
和 filter()
是最常见的迭代器适配器。
let v1: Vec<i32> = vec![1, 2, 3];
// map 是迭代器适配器,collect 是消耗型适配器
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
进阶示例:自定义迭代器
struct Counter { current: u32, end: u32 }
impl Counter { fn new(end: u32) -> Self { Self { current: 0, end } } }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end { self.current += 1; Some(self.current) } else { None }
}
}
fn main() {
let sum: u32 = Counter::new(5).map(|x| x * 2).sum();
assert_eq!(sum, 30);
}
70. Deref
trait 是如何工作的?
答:
Deref
trait 允许你自定义解引用运算符 *
的行为。通过为类型实现 Deref
trait,你可以让它像一个常规引用一样工作。
当对一个实现了 Deref
的类型使用 *
时,Rust 实际上会调用 *self.deref()
。这使得我们可以编写能够同时处理智能指针和普通引用的代码。这个特性被称为解引用强制多态 (Deref Coercions)。
进阶示例:实现 Deref
让自定义类型像引用
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> { fn new(x: T) -> Self { MyBox(x) } }
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.0 }
}
fn hello(name: &str) { println!("Hello, {}", name); }
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m); // Deref 强制从 &MyBox<String> -> &String -> &str
}
71. Drop
trait 是如何工作的?
答:
Drop
trait 允许你在一个值离开作用域时执行一些代码。它通常用于释放资源,比如文件句柄、网络连接或像 Box<T>
那样释放堆内存。
你只需要实现 drop
方法。Rust 会在值需要被清理时自动调用它。你不能手动调用 drop
方法。
进阶示例:Drop
资源释放与自定义清理
struct Connection { id: u32 }
impl Drop for Connection {
fn drop(&mut self) {
eprintln!("closing connection {}", self.id);
}
}
fn main() {
let _c = Connection { id: 1 }; // 作用域结束时自动调用 drop
}
72. 什么是 Trait Object?
答: Trait object 允许你使用一个指向实现了某个 trait 的类型的指针。它是一种在运行时使用多态的方式。你可以创建一个包含不同类型值的 vector,只要这些值都实现了同一个 trait。
Trait object 通过 &dyn Trait
或 Box<dyn Trait>
的形式表示。dyn
关键字表明这是一个动态分发的 trait object。
进阶示例:动态分发集合
trait Animal { fn speak(&self) -> String; }
struct Dog; struct Cat;
impl Animal for Dog { fn speak(&self) -> String { "woof".into() } }
impl Animal for Cat { fn speak(&self) -> String { "meow".into() } }
fn main() {
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for a in zoo { println!("{}", a.speak()); }
}
73. 动态分发和静态分发有什么区别?
答:
- 静态分发 (Static Dispatch): 这是 Rust 默认的方式。当代码使用泛型和 trait bound 时,编译器在编译时会为每个具体类型生成一份专门的代码。这被称为“单态化 (monomorphization)”。它的优点是速度快,因为没有运行时开销。
- 动态分发 (Dynamic Dispatch): 这是通过 trait object (dyn Trait
) 实现的。在运行时,程序会通过查找虚函数表(vtable)来确定应该调用哪个方法。它的优点是代码尺寸更小,并且允许你在一个集合中存储不同类型的值。缺点是存在轻微的运行时性能开销。
示意图:静态分发与动态分发对比
flowchart LR
A[泛型函数 T: Trait] -->|编译期| M[单态化 多份机器码]
B[&dyn Trait] -->|运行期| V[vtable 查找]
74. AsRef
和 AsMut
trait 有什么用?
答:
AsRef<T>
和 AsMut<T>
是用于廉价的、引用到引用的转换的 trait。
如果一个类型 U
实现了 AsRef<T>
,意味着你可以通过调用 .as_ref()
方法,从 &U
廉价地得到一个 &T
。这在编写希望接受多种不同但相关引用类型的函数时非常有用。例如,一个函数可以接受任何能被看作 &str
的类型(如 String
, &String
, &str
)。
进阶示例:通用路径参数
use std::path::Path;
fn read_all<P: AsRef<Path>>(p: P) {
let path = p.as_ref();
println!("reading {:?}", path);
}
fn main() {
read_all("/tmp/a.txt");
read_all(String::from("/tmp/b.txt"));
}
75. 什么是 newtype 模式?
答: Newtype 模式是在 Rust 中使用元组结构体来包装一个现有类型,从而创建一个新的、独特的类型。
struct Millimeters(u32);
struct Meters(u32);
Millimeters
类型的值和 Meters
类型的值混用,即使它们内部都是 u32
。
2. 抽象: 你可以为这个新类型实现它自己独有的方法和 trait,而不用去修改原始类型。
进阶示例:为 newtype 实现外部 trait
use std::fmt::{self, Display};
struct Millimeters(u32);
impl Display for Millimeters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}mm", self.0)
}
}