跳转至

第四章:高级特性

本章将深入探讨使 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)?

答: 智能指针是一类数据结构,它们的行为类似于指针,但还拥有额外的元数据和能力。它们通常通过实现 DerefDrop trait 来实现这些功能。

  • Deref trait 允许智能指针实例的行为像引用一样,这样你就可以编写既适用于智能指针也适用于普通引用的代码。
  • Drop trait 允许你自定义当智能指针实例离开作用域时运行的代码,通常用于释放资源。

StringVec<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));
闭包在 Rust 中被广泛用于迭代器和线程等场景。

进阶示例: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. 闭包如何捕获环境?有哪几种方式?

答: 闭包可以通过三种方式捕获变量,这三种方式对应 FnFnMutFnOnce 三个 trait。

  1. FnOnce: 闭包会获取变量的所有权Once 表示它至少需要获取所有权,因此只能被调用一次。
  2. FnMut: 闭包会可变地借用变量。Mut 表示它可以改变环境中的变量。
  3. 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 TraitBox<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. AsRefAsMut 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);
这样做的好处是: 1. 类型安全: 你不能意外地将 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)
    }
}