DanLevy.net

测验:Rust 内存管理要点

(借用)先自查,别自爆!🦀

准备好测试你的 Rust 内存管理技能了吗?🦀

本测验将挑战你对 Rust 所有权系统、借用规则、生命周期和智能指针的理解。

注意: 题目以约 50 列宽度排版,确保在所有设备上清晰可读。(欢迎提出改进建议!)

无论你是经验丰富的 Rustacean,还是刚接触内存管理的新手,这个测验都能帮你巩固知识。让我们开始吧! 🦀

运行这段代码会发生什么?尝试预测输出或错误:

fn main() {
let philosopher =
String::from("Zeno of Citium");
let greeting = philosopher;
println!("Hello, {}!", philosopher);
}

这段代码无法编译,因为 Rust 的所有权规则。当我们把 philosopher 赋值给 greeting 时,字符串的所有权被移动到了 greeting。移动之后,philosopher 就不再有效了。

这里有三种修复方法:

  1. 克隆字符串(创建一个新副本):
let greeting = philosopher.clone();
  1. 使用引用(借用值):
let greeting = &philosopher;
  1. 使用字符串切片(借用字符串的一部分):
let greeting = &philosopher[..];

每种解决方案都有不同的用例和性能影响。克隆更昂贵但能获得所有权,而引用更便宜但有生命周期约束。

运行这段代码会发生什么?思考所有权转移:

fn take_knowledge(knowledge: String) {
println!("Knowledge: {}", knowledge);
}
fn main() {
let wisdom = String::from("know thyself");
take_knowledge(wisdom);
// What happens to our wisdom?
println!("Do you {}", wisdom);
}

代码无法编译,因为wisdom的所有权已移动到take_knowledge,因此之后无法使用。

以下是修复此问题的三种方法:

  1. 通过引用传递(借用值):
fn borrow_it(text: &String) {
println!("Inside: {}", text);
}
borrow_it(&wisdom); // Now wisdom can be used after
  1. 克隆值(创建新副本):
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. 从函数返回所有权:
fn take_and_return(text: String) -> String {
println!("Inside: {}", text);
text // Return ownership back
}
let wisdom = take_and_return(wisdom); // Reassign returned ownership

每种方法都有不同的用例:

  • 引用:最高效,但需要生命周期管理
  • 克隆:简单但可能代价高昂
  • 返回所有权:适用于转换值

最佳实践:除非需要所有权转移,否则使用引用。

多个可变引用会发生什么?

fn main() {
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom; // First mutable borrow
let ref2 = &mut wisdom; // Second mutable borrow
ref1.push_str(" himself never runs");
ref2.push_str(" out of things to laugh at.");
}

思考 Rust 关于可变引用的规则。

这段代码违反了 Rust 的基本借用规则:

  • 同一时间只能有一个可变引用指向一个值
  • 或者任意数量的不可变引用
  • 引用不能超过其引用的对象的生命周期

以下是修复代码的方法:

  1. 使用顺序作用域:
let mut wisdom = String::from("He who laughs at");
{
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs");
} // ref1 goes out of scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. 或者在单个借用中修改字符串:
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs out of things to laugh at.");

这些规则在编译时防止数据竞争,使 Rust 默认线程安全。

常见陷阱:试图使用多个可变引用来避免克隆或同时修改同一值的不同部分。

这段代码能编译吗?如果能,为什么?如果不能,哪里有问题?

fn first_word(s: &str) -> &str { // No explicit lifetimes?
match s.find(' ') {
Some(pos) => &s[0..pos],
None => s,
}
}
fn main() {
let name = String::from("Seneca the Younger");
let first = first_word(&name);
println!("Hello, {}", first);
}

这段代码编译成功,得益于 Rust 的生命周期省略规则。 这些规则允许编译器在常见模式中自动推断生命周期。

三条生命周期省略规则是:

  1. 每个参数都有自己的生命周期参数
  2. 如果只有一个输入生命周期参数,则该生命周期被分配给所有输出生命周期参数
  3. 如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,则 self 的生命周期被分配给所有输出生命周期参数

这个函数等价于:

fn first_word<'a>(s: &'a str) -> &'a str {
// ... same implementation
}

省略规则适用的常见模式:

// These don't need explicit lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

最佳实践:尽可能让省略规则为你工作,但要理解何时需要显式生命周期。

这个递归类型定义有什么问题?

#[derive(Debug)]
enum CatList {
Cons(i32, CatList), // Recursive without indirection
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
CatList::Cons(2,
CatList::Cons(3,
CatList::Nil)));
}

这段代码失败是因为编译器无法在编译时确定 CatList 的大小。该类型的递归特性意味着它可能是无限大的!

以下是使用 Box<T> 修复的方法:

#[derive(Debug)]
enum CatList {
Cons(i32, Box<CatList>), // Box provides a fixed-size pointer
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
Box::new(CatList::Cons(2,
Box::new(CatList::Cons(3,
Box::new(CatList::Nil))))));
}

为什么 Box<T> 有效:

  1. Box 提供了一个固定大小的指针(在 64 位系统上通常为 8 字节)
  2. 实际数据存储在堆上
  3. 编译器现在确切知道需要分配多少空间

Box<T> 的常见用例:

  • 递归数据结构(链表、树)
  • 需要确保堆分配的大型数据
  • 需要动态分发的 trait 对象

最佳实践:在以下情况下使用 Box<T>:

  • 递归类型
  • 确保堆分配
  • 移动大型数据而不进行复制

这段代码会打印什么?仔细计数!

use std::rc::Rc;
fn main() {
let text = Rc::new(String::from("Meditations")); // Count: 1
let marcus = Rc::clone(&text); // What happens here?
let aurelius = Rc::clone(&text); // And here?
println!(
"Reference count: {}",
Rc::strong_count(&text)
);
}

让我们分解 Rc 的工作原理:

  1. 使用 Rc::new() 初始创建:计数 = 1
  2. 第一次克隆给 marcus:计数 = 2
  3. 第二次克隆给 aurelius:计数 = 3

重要的 Rc 特性:

use std::rc::Rc;
fn demonstrate_rc() {
let original = Rc::new(String::from("Shared"));
println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{
let copy = Rc::clone(&original);
println!("Count inside scope: {}", Rc::strong_count(&original)); // 2
} // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1
}

关键点:

  • Rc::clone() 很廉价——它只增加一个计数器
  • Rc 仅适用于单线程场景
  • 当最后一个引用被丢弃时,数据会被清理
  • 使用 Weak 引用来防止引用循环

最佳实践:

  • 当你需要共享所有权时使用 Rc
  • 对于线程安全场景考虑使用 Arc
  • 避免创建引用循环

这个结构体定义能编译通过吗?为什么能或为什么不能?

struct Philosopher {
name: &str, // Reference without lifetime
quote: &str, // Another reference without lifetime
}
fn main() {
let phil = Philosopher {
name: "Seneca",
quote: "Luck happens when preparation meets opportunity",
};
}

代码无法编译,因为包含引用的结构体必须指定生命周期。以下是修复方法:

// Single lifetime parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

常见模式:

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

最佳实践:

  1. 当需要无限期存储数据时,使用所有权类型(如 String)
  2. 当结构体的生命周期明显短于数据时,使用引用
  3. 当引用可能具有不同生命周期时,考虑使用多个生命周期参数
  4. 在复杂结构中记录生命周期关系

这个返回两个字符串切片中较长者的函数会发生什么?

fn longest(text1: &str, text2: &str) -> &str {
if text1.len() > text2.len() {
text1 // Returning a reference, but which lifetime?
} else {
text2 // Could be this reference instead
}
}
fn main() {
println!("{}", longest(
"Seneca the Younger",
"Marcus Aurelius"
));
}

返回的引用必须处于对两个输入都有效的生命周期内,不能比任一被借用的值活得更久。生命周期注解描述这种关系,并不延长生命周期。

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}

这段代码运行时会发生什么?

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow_mut(); // First mutable borrow
let _borrow2 = data.borrow_mut(); // Second mutable borrow
println!("Value: {}", _borrow2);
}

原代码在第一个可变借用仍然存活时调用第二次 borrow_mut(),因此触发运行时 panic,还没有执行到打印语句。修复时,应先释放现有可变借用,再创建其他借用;下面的例子在创建共享借用前显式释放可变借用。

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let mut second = data.borrow_mut();
*second += 1;
drop(second);
let read1 = data.borrow();
let read2 = data.borrow();
println!("{} {}", *read1, *read2);
}

这段代码会输出什么?

use std::cell::Cell;
fn main() {
let life = Cell::new(42);
let meaning = &life; // Shared reference
println!("{}", life.get()); // What prints here?
meaning.set(43); // Mutation through shared ref
println!("{}", life.get()); // And here?
}

Cell 和 RefCell 为内部可变性服务不同的目的:

use std::cell::{Cell, RefCell};
// Cell for Copy types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

关键区别:

  1. Cell:
  • 最适合 Copy 类型
  • 没有借用 API
  • 总是复制或移动值
  1. RefCell:
  • 适用于任何类型
  • 有借用 API
  • 运行时借用检查

最佳实践:

  1. 对简单的 Copy 类型(数字、布尔等)使用 Cell
  2. 当需要借用内容时使用 RefCell
  3. 通过 Cell/RefCell 的修改保持最小化
  4. 记录为什么需要内部可变性

何时在 Rust 中使用 Rc(引用计数)?

考虑以下示例:

use std::rc::Rc;
struct SharedConfig {
name: String,
value: i32,
}
fn main() {
let config = Rc::new(SharedConfig {
name: "settings".to_string(),
value: 42,
});
let config2 = Rc::clone(&config);
// Both config and config2 share ownership
}

Rc(引用计数)专为需要共享所有权的单线程场景设计。

常见用例:

use std::rc::Rc;
use std::cell::RefCell;
// Shared ownership in data structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

要点:

  1. 在以下情况使用 Rc:
  • 代码的多个部分需要所有权
  • 你知道共享是单线程的
  • 生命周期无法静态确定
  1. 在以下情况改用 Arc:
  • 需要线程安全的共享
  • 多个线程需要所有权
  1. Rc 的限制:
  • 非线程安全
  • 轻微运行时开销
  • 无法自动打破引用循环

最佳实践:

  1. 尽可能优先使用唯一所有权
  2. 对单线程共享所有权使用 Rc
  3. 对多线程场景使用 Arc
  4. 结合 Weak 以防止引用循环

Rust中RefCell和RwLock的关键区别是什么?

考虑以下示例:

use std::cell::RefCell;
use std::sync::RwLock;
// Example 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let shared = RwLock::new(vec![1, 2, 3]);
let locked = shared.write().unwrap();

RefCell<T> 在运行时检查借用,不实现 Sync,因此不能跨线程共享它的引用。但在 T: Send 时,可以把值移动到另一个线程。RwLock<T> 在相应类型约束下提供同步访问。

这段代码运行时会怎样?

use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(42));
let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock
let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);
}

在同一线程中,未释放第一个 guard 就再次锁定同一 mutex,不会正常返回。API 允许死锁或 panic。再次加锁前应释放 guard。

use std::sync::Mutex;
fn main() {
let lock = Mutex::new(42);
{ let mut guard = lock.lock().unwrap(); *guard += 1; }
let guard = lock.lock().unwrap();
println!("{}", *guard);
}

当你运行这段使用弱引用的代码时会发生什么?

use std::rc::{Rc, Weak};
fn main() {
let data = Rc::new(String::from("Wisdom"));
let weak = Rc::downgrade(&data); // Create weak reference
drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());
}

弱引用不会阻止其目标被释放。以下是一个详细示例:

use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child tree structure avoiding reference cycles
struct Node {
next: Option<Rc<Node>>,
parent: RefCell<Weak<Node>>, // Weak to prevent cycles
value: i32,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
next: None,
parent: RefCell::new(Weak::new()),
value,
})
}
fn set_parent(&self, parent: &Rc<Node>) {
*self.parent.borrow_mut() = Rc::downgrade(parent);
}
fn get_parent(&self) -> Option<Rc<Node>> {
self.parent.borrow().upgrade()
}
}

常见用例:

  1. 类似缓存的结构,条目可以被清除
  2. 具有父引用的树结构
  3. 观察者模式,主题可以被丢弃
  4. 打破复杂数据结构中的引用循环

最佳实践:

  1. 对可选关系使用弱引用
  2. 在使用前检查 upgrade() 的结果
  3. 清晰地记录所有权关系
  4. 对于简单情况考虑使用索引等替代方案

在这个 RAII 示例中,文件句柄会发生什么?

use std::fs::File;
struct FileWrapper {
file: File,
}
fn main() {
let file = File::create("test.txt").unwrap();
let wrapper = FileWrapper { file };
// ... use wrapper ...
// No Drop implementation
}

Rust 中的 RAII 确保资源得到正确管理。在此示例中,FileWrapper 不需要自定义 Drop 实现来关闭文件句柄:当包装器离开作用域时,其 File 字段会自动丢弃。

只有当包装器本身有超出其字段丢弃的额外清理行为时,才需要实现 Drop:

use std::fs::File;
use std::io::{self, Write};
struct FileWrapper {
file: File,
path: String,
}
impl FileWrapper {
fn new(path: &str) -> io::Result<FileWrapper> {
Ok(FileWrapper {
file: File::create(path)?,
path: path.to_string(),
})
}
fn write(&mut self, content: &str) -> io::Result<()> {
self.file.write_all(content.as_bytes())
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Ensure file is properly closed
// Could also do cleanup like deletion
println!("Closing file: {}", self.path);
}
}

RAII 模式:

  1. 构造函数获取资源
  2. 方法安全地使用资源
  3. 当所有者离开作用域时,字段自动丢弃
  4. 自定义 Drop 在需要时添加额外清理
  5. 使用 ? 进行错误传播

最佳实践:

  1. 当标准库 Drop 实现已经管理资源时,依赖它们
  2. 保持资源管理简单明了
  3. 尽可能使用标准库类型
  4. 记录清理行为
  5. 考虑使用守卫模式进行作用域操作

当我们克隆这个 Philosophy 结构体时会发生什么?

#[derive(Clone)]
struct Philosophy {
school: String,
founder: String,
}
fn main() {
let stoicism = Philosophy {
school: String::from("Stoicism"),
founder: String::from("Zeno of Citium")
};
let new_school = stoicism.clone();
println!("{} - {}",
stoicism.school, new_school.school);
}

Copy 是隐式的按位复制。Clone 需要显式调用,可能分配内存;这里两个 String 字段各自得到独立副本。值在栈上还是堆上,并不决定类型能否实现 Copy。

在典型的当前 64 位 Rust 目标上,这个结构体的大小是多少?

struct Metadata {
id: u32, // How many bytes?
name: String, // How many bytes?
active: bool // How many bytes + padding?
}

让我们分解结构体内存布局和优化:

// Typical current 64-bit Rust layout: 32 bytes
struct Metadata {
id: u32, // 4 bytes
name: String, // 24 bytes on 64-bit systems
active: bool // 1 byte + padding/alignment
}
// Reordering fields may reduce padding for repr(C) structs,
// but default Rust layout is not a stable ABI guarantee.
struct OptimizedMetadata {
name: String, // 24 bytes
id: u32, // 4 bytes
active: bool // 1 byte + 3 padding
}
// Further optimization with packing
#[repr(packed)]
struct PackedMetadata {
id: u32,
active: bool,
name: String,
}

内存布局考虑:

  1. 对齐要求:
  • u32: 4 字节对齐
  • String: 在常见 64 位目标上为 8 字节对齐和 24 字节大小
  • bool: 1 字节对齐
  1. 字段排序策略:
  • 将相似大小的字段分组
  • 将较大对齐的字段放在前面
  • 考虑缓存行优化

最佳实践:

  1. 对于 FFI 或稳定布局假设,使用适当的 repr(...)
  2. 使用适当的整数大小
  3. 考虑对可选字段使用 Option
  4. 使用 std::mem::size_of 测量对大小敏感的结构体
  5. 谨慎使用 #[repr(packed)] - 它可能影响性能

启用优化时,Rust 的零成本迭代器抽象能让这两种实现达到什么效果?

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn sum_loop(v: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..v.len() {
sum += v[i];
}
sum
}

零成本抽象允许生成高效的优化代码,但不保证每种编译器和优化级别下耗时完全相同。两个函数计算相同的和。性能重要时,应测量实际工作负载。

use std::ops::Range;
trait ZeroCost {
fn process(&self) -> u32;
}
impl ZeroCost for Range<u32> {
fn process(&self) -> u32 {
self.clone().fold(0, |acc, x| acc + x)
}
}

感谢参与测验!如果你喜欢测试自己的 Rust 知识,欢迎查看我的其他编程挑战!🧠

想提升你的 Rust 技能吗? 以下是一些推荐资源: