Rust 智能指针
Rc<T>
Rc<T> 是一种引用计数智能指针,允许在多个所有者之间共享数据。它在单线程环境中非常有用,但在多线程环境中需要使用 Arc<T>。
使用 Rc<T> 时,克隆一个 Rc 实际上是增加了引用计数,而不是复制数据。这使得它非常高效,但需要注意避免循环引用。
可以通过 Rc::strong_count(&rc) 来查看当前的引用计数。
// Define a var with `Rc<T>`
let a = Rc::new(String::from("Hello, Rust!"));
println!("String ref: {}", Rc::strong_count(&a));
{
#[allow(unused_variables)]
let b = Rc::clone(&a);
println!("String ref: {}", Rc::strong_count(&a));
}
println!("String ref: {}", Rc::strong_count(&a));
上方示例中发现通过 Rc::clone() 建立了对 a 的引用,此引用计数增加了 1。当 b 超出作用域时,引用计数又减少了 1。
String ref: 1
String ref: 2
String ref: 1
此外,Rc<T>还用于实现树形数据结构,这样相较于使用纯引用的优点是可以在树的不同部分共享数据,而不需要担心生命周期问题。
先定义一个 List 枚举来表示一个简单的链表:
#[derive(Debug)]
enum List {
Cons(i32, Rc<List>),
Nil,
}
通过 Rc<T>,我们可以创建一个链表,其中的节点可以被多个所有者共享。建立链表时要注意保证类型为 Rc<List>,这样可以在多个节点之间共享数据。如果只是裸 List 类型,那么每个节点的所有权是唯一的,无法共享,错误 expected &Rc<List, Global>, found &List (rust-analyzer E0308)。
println!("List sample:");
// Link b -> a
let a = Rc::new(Cons(1, Rc::new(Nil)));
let b = Rc::new(Cons(2, Rc::clone(&a)));
// Link c -> b -> a; d -> b -> a
let c = Rc::new(Cons(3, Rc::clone(&b)));
let d = Rc::new(Cons(4, Rc::clone(&b)));
// Now c, d both point to b, which points to a. The reference counts are as follows:
// a: 2
// b: 3
// c: 1
// d: 1
println!(
"Ref count: a: {}, b: {}, c: {}, d: {}",
Rc::strong_count(&a),
Rc::strong_count(&b),
Rc::strong_count(&c),
Rc::strong_count(&d)
);
println!("List a: {:?}", a);
println!("List b: {:?}", b);
println!("List c: {:?}", c);
println!("List d: {:?}", d);
这样可以对树形数据结构方便地进行操作,例如计算链表中所有元素的和:
println!("Sum of link c: {}", c.sum());
实现一下 sum 方法,递归求和:
impl List {
fn sum(&self) -> i32 {
match self {
Cons(value, next) => value + next.sum(),
Nil => 0,
}
}
}
Arc<T>
由于 Rc<T> 不能在多线程环境中使用,因此 Rust 提供了 Arc<T>,它是 Rc<T> 的线程安全版本。Arc<T> 使用原子引用计数来确保在多线程环境中安全地共享数据。
由于原子操作的开销,Arc<T> 的性能可能比 Rc<T> 略低,但它在多线程环境中是必需的。所以在单线程中,建议使用 Rc<T>,而在多线程中则使用 Arc<T>。
Arc<T> 同 Rc<T> 一样,克隆一个 Arc 也只是增加引用计数,而不是复制数据,并且也不可变内部数据。
Rust 标准库提供了 std::thread 模块,可以轻松地创建和管理线程。下面是一个简单的示例,展示如何使用 Arc<T> 在多线程环境中共享数据。
先按照 Rc<T> 的方式创建 Arc<T>,并且使用它们。
println!("Arc<T>");
let a = Arc::new(String::from("Hello, Rust!"));
#[allow(unused_variables)]
let b = Arc::clone(&a);
println!("String ref: {}", Arc::strong_count(&a));
{
#[allow(unused_variables)]
let c = Arc::clone(&a);
println!("String ref: {}", Arc::strong_count(&a));
}
Arc<T>
String ref: 2
String ref: 3
此时的结果与 Rc<T> 相同。
现在引入多线程环境,使用 std::thread 模块创建线程,并在每个线程中克隆 Arc<T>。
let mut handles = vec![];
for i in 0..8 {
let a_clone = Arc::clone(&a);
handles.push(thread::spawn(move || {
#[allow(unused_variables)]
let d = Arc::clone(&a_clone);
println!("String ref in thread {i}: {}", Arc::strong_count(&a_clone));
}));
}
for h in handles {
h.join().unwrap();
}
println!("String ref out of threads: {}", Arc::strong_count(&a));
此时在每个线程中,Arc<T> 的引用计数会增加,并且在所有线程完成后,引用计数会恢复到原来的值。
由于线程的并发性质,输出的引用计数是基本随机的。
String ref in thread 0: 7
String ref in thread 4: 11
String ref in thread 2: 11
String ref in thread 3: 12
String ref in thread 1: 11
String ref in thread 5: 12
String ref in thread 6: 13
String ref in thread 7: 12
String ref out of threads: 2
此处需要特别注意,创建多线程环境时如果直接使用 thread::spawn(move || { ... }).join().unwrap(),那么在闭包中使用的变量必须是 Send 的类型。并且要确保在闭包中使用的 Arc<T> 是克隆后的。由于 Arc<T> 无法使用 Send,所以创建多个线程时必须把建立的线程 push 到一个线程处理向量里面,然后使用 join() 函数分别启动线程。错误 the type Arc does not implement Copy. consider using Arc::clone (rustc E0382)。
Box<T>
Box<T> 是一种智能指针,用于在堆上分配数据。它提供了一个指向堆上数据的指针,并且在 Box<T> 超出作用域时会自动释放堆上的内存。Box<T> 主要用于以下几种情况:
- 递归类型:当你需要定义一个递归类型时,
Box<T>可以用来包装递归结构的成员,以避免无限大小的问题。 - 动态大小类型:当你需要在编译时无法确定大小的类型时,
Box<T>可以用来包装这些类型。 - 性能优化:当你需要在堆上分配数据以减少栈上的内存使用时,
Box<T>可以用来实现这一点。
Box<T> 一般可以直接使用。
let a = Box::new(String::from("FlyCat..."));
println!("String in box: {}", a);
之前使用 Rc<T> 创建了具有多个所有权的链表,但是 Box<T> 只能有一个所有者,据此可以建立单所有权链表,这样的链表一般用于实现栈结构。
enum List {
Cons(i32, Box<List>),
Nil,
}
impl List {
fn sum(&self) -> i32 {
match self {
List::Cons(value, next) => value + next.sum(),
List::Nil => 0,
}
}
}
然后使用:
let stack_list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
let total = stack_list.sum();
println!("Sum of stack_list: {}", total);
输出:
Sum of stack_list: 6
此外,更常用的是用于实现动态大小类型,例如 dyn Trait,或者在递归类型中使用 Box<T> 来避免无限大小的问题。dyn Trait 是一种动态分发的特征对象,可以在运行时确定其具体类型,而 Box<dyn Trait> 则允许我们在堆上存储这些动态类型,如果内部类型不确定时可以使用。
定义统一 Trait,使用 struct 附着实现 Trait:
trait Cute {
fn speak(&self);
}
struct Cat;
struct Dog;
struct NyanCat;
impl Cute for Cat {
fn speak(&self) {
println!("Meow!");
}
}
impl Cute for Dog {
fn speak(&self) {
println!("Woof!");
}
}
impl Cute for NyanCat {
fn speak(&self) {
println!("NyanNyanNyanNyanNyanNyanNyanNyan~");
}
}
然后使用 Box<dyn Cute> 来存储这些不同类型的对象:
let cute_stream: Vec<Box<dyn Cute>> = vec![
Box::new(Cat),
Box::new(Dog),
Box::new(NyanCat),
Box::new(Cat),
Box::new(NyanCat),
Box::new(NyanCat),
Box::new(Dog),
Box::new(NyanCat),
];
然后就可以通过迭代 cute_stream 来调用 speak 方法:
cute_stream.iter().for_each(|i| i.speak());
Meow!
Woof!
NyanNyanNyanNyanNyanNyanNyanNyan~
Meow!
NyanNyanNyanNyanNyanNyanNyanNyan~
NyanNyanNyanNyanNyanNyanNyanNyan~
Woof!
NyanNyanNyanNyanNyanNyanNyanNyan~
此外,Box<T> 还可以用于优化性能,例如在函数中返回一个大的数据结构时,使用 Box<T> 可以避免在栈上分配大量内存,从而提高性能。
如果有时想要在内存里面放一个分辨率高达 $1024 * 1024 * 1024$ 的一个方块,这样内存占用会到达 $(1024 * 1024 * 1024) * 4$ 字节(假设每个像素占用 4 字节,共约 4 GB),这时就可以使用 Box<T> 来在堆上分配这个大数据结构,而不是在栈上分配。
let huge_block = Box::new(vec![vec![vec![0; 1024]; 1024]; 1024]);
RefCell<T>
RefCell<T> 是一种在运行时进行可变借用检查的智能指针。它允许在不可变引用的情况下修改数据,但会在运行时检查借用规则,确保不会出现数据竞争或悬垂引用。
简单来说,就是在不可变的情况下进行可变借用。
但是,如果此时违反了借用规则,例如在同一时间内有多个可变借用,或者在有不可变借用的情况下尝试进行可变借用,RefCell<T> 会在运行时抛出一个 panic。这样的操作必须人为地保证不会发生,否则程序会在运行时崩溃。
use std::cell::RefCell;
fn main() {
let a = RefCell::new(String::from("Hello, Rust!"));
{
let mut b = a.borrow_mut();
b.push_str(" Welcome to RefCell.");
}
println!("String in RefCell: {}", a.borrow());
}
如果内部变成:
let mut b = a.borrow_mut();
b.push_str(" Welcome to RefCell.");
let c = a.borrow();
println!("String in RefCell: {}", c);
运行时就会抛出 panic:
thread 'main' (49860) panicked at examples\refcell.rs:9:19:
RefCell already mutably borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\examples\refcell.exe` (exit code: 101)
为了避免这个不经意的乱引用的情况,Rust 提供了方法 try_borrow 和 try_borrow_mut,它们会返回一个 Result 类型,如果借用成功则返回 Ok,如果借用失败则返回 Err,这样就可以在运行时安全地处理借用错误。
let mut b = a.borrow_mut();
b.push_str(" Welcome to RefCell.");
let c = a.try_borrow();
c.ok().map(|c| println!("String in RefCell: {}", c));
上述示例中,try_borrow 返回了一个 Result,如果借用失败则返回 Err,否则返回 Ok。通过这种方式,我们可以在运行时安全地处理借用错误,而不会导致程序崩溃。同时由于这里本身发生错误,所以 c.ok() 返回了 None,因此不会打印出任何内容。
Cell<T>
Cell<T> 是一种在运行时进行内部可变性的智能指针。它允许在不可变引用的情况下修改数据,但与 RefCell<T> 不同的是,Cell<T> 通过复制数据来实现内部可变性,而不是通过借用检查。由于 Cell<T> 只能用于实现 Copy trait 的类型,因此它的使用场景相对有限。
如果对一个没有实现 Copy trait 的类型使用 Cell<T>,会导致编译错误。例如:
let a = Cell::new(String::from("value"));
let b = a.clone();
错误
the method clone exists for struct Cell<String>, but its trait bounds were not satisfied
the following trait bounds were not satisfied:
String: Copy
which is required by Cell<String>: Clone (rustc E0599)
可以对实现了 Copy trait 的类型使用 Cell<T>,例如:
fn main() {
let a = Cell::new(5);
let b = a.clone();
b.set(4);
println!("b = {}", b.get());
}
输出:
b = 4
注意此处使用的 set() 方法来修改 Cell<T> 中的值,而不是直接修改它。这是因为 Cell<T> 通过复制数据来实现内部可变性,因此需要使用 set() 方法来更新值。get() 方法则用于获取 Cell<T> 中的值。
此处的 clone() 方法实际上是 Cell<T> 的 Clone trait 实现,它会创建一个新的 Cell<T>,并将原来的值复制到新的 Cell<T> 中。
Cow<T>
Cow<T> 是一种在运行时进行克隆的智能指针。它允许在需要时克隆数据,而不是在编译时就进行克隆。Cow<T> 的全称是 “Clone on Write”,它的设计目的是为了在需要修改数据时才进行克隆,以节省内存和提高性能。
这个例子演示了 Cow(Clone on Write)的核心用途:在同一类型里既能持有借用数据,又能持有自有数据,延迟分配直到真正需要修改时。
fn main() {
let a: Cow<String> = Cow::Owned(String::from("Hello, world!"));
println!("a = {}", a);
let mut handles = vec![];
for t in 0..30 {
let handle = thread::spawn(move || {
let say = if t % 2 == 0 {
Cow::Borrowed("Hello, world!")
} else {
Cow::Owned(format!("Hello, world! from thread {t}"))
};
println!("thread {t} say = {}", say);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
}
线程里面的 Cow 变量 say,在偶数线程中是借用的字符串,而在奇数线程中是拥有的字符串。这样可以在需要修改数据时才进行克隆,从而提高性能。读多写少的场景下,此智能指针在大部分路径零分配,只有真正需要修改时才付出克隆成本。
Weak<T>
这个指针类型是 Rc<T> 和 Arc<T> 的弱引用版本。它允许在不增加引用计数的情况下引用数据,从而避免循环引用的问题。Weak<T> 不会阻止数据被释放,因此在使用 Weak<T> 时需要检查它是否仍然有效。
典型的使用场景是在树形数据结构中需要对节点进行父子关系的引用时,使用 Weak<T> 来避免循环引用。例如,在一个树形结构中,子节点可以持有对父节点的 Weak<T> 引用,而父节点持有对子节点的 Rc<T> 引用。这样,当父节点被释放时,子节点的 Weak<T> 引用不会阻止父节点被释放,从而避免了内存泄漏。
我们尝试用这个东西来写一个简单的 HTML DOM 转 Markdown 的程序,来看看 Weak<T> 的使用。
首先把 Element 定义成一个结构体,包含一个 name 字段表示标签名称,一个 children 字段表示子节点列表,以及一个 parent 字段表示父节点的弱引用。这里引入了一个未被使用的 id 字段,来模拟实际应用中可能存在的其他字段。
use std::{cell::RefCell, rc::Rc, sync::Weak};
struct Node {
#[allow(unused)]
id: i32,
name: RefCell<String>,
#[allow(unused)]
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
context: RefCell<Option<String>>
}
然后把他们给链接起来:
let html = Rc::new(Node::new(0, "html"));
let head = Rc::new(Node::new(1, "head"));
let title = Rc::new(Node::new(3, "title"));
let script = Rc::new(Node::new(4, "script"));
let body = Rc::new(Node::new(2, "body"));
let h1 = Rc::new(Node::new(5, "h1"));
title.context.replace(Some(String::from("My Page")));
h1.context.replace(Some(String::from("Hello, world!")));
script.context.replace(Some(String::from("console.log('Hello, world!');")));
// Link nodes
html.children.borrow_mut().push(Rc::clone(&head));
html.children.borrow_mut().push(Rc::clone(&body));
head.children.borrow_mut().push(Rc::clone(&title));
body.children.borrow_mut().push(Rc::clone(&script));
body.children.borrow_mut().push(Rc::clone(&h1));
使用了 new() 方法,实现一下。
impl Node {
fn new(id: i32, name: &str) -> Self {
Node {
id,
name: RefCell::new(String::from(name)),
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
context: RefCell::new(None)
}
}
}
然后实现一个简陋的渲染 Markdown 的方法:
fn display_markdown(&self) {
if self.name.borrow().as_str() == "html" {
for child in self.children.borrow().iter() {
if child.name.borrow().as_str() == "head" {
for head_child in child.children.borrow().iter() {
if head_child.name.borrow().as_str() == "title" {
if let Some(title) = head_child.context.borrow().as_ref() {
println!("# {}", title);
}
}
}
} else if child.name.borrow().as_str() == "body" {
for body_child in child.children.borrow().iter() {
if body_child.name.borrow().as_str() == "h1" {
if let Some(h1) = body_child.context.borrow().as_ref() {
println!("## {}", h1);
}
} else if body_child.name.borrow().as_str() == "script" {
if let Some(script) = body_child.context.borrow().as_ref() {
println!("```js\n{}\n```", script);
}
}
}
}
}
} else {
println!("Please generate a markdown context from a `html` Node!");
}
}
这里渲染过程其实完全用不到 Weak<T>,但是在实际应用中,如果需要在节点之间建立父子关系,并且需要避免循环引用,那么 Weak<T> 就非常有用。
调用:
html.display_markdown();
这样就能够把简单的 HTML DOM 转换成 Markdown 格式了。
假如我需要知道 h1 的父节点是什么,那么就可以使用 Weak<T> 来获取父节点的引用:
println!(
"The parent of `h1` is {}.",
h1
.parent
.borrow()
.upgrade()
.map_or(
"None"
.to_string(),
|p| p.name.borrow()
.clone()
)
);
只是这样还不行,因为还没有把 h1 的父节点设置为 body,写一个实现来设置父节点的方法:
fn auto_set_parent(self: &Rc<Self>) {
for child in self.children.borrow().iter() {
child.parent.replace(Rc::downgrade(self));
child.auto_set_parent();
}
}
此函数自动迭代所有子节点,并将它们的父节点设置为当前节点的弱引用。调用:
html.auto_set_parent();
然后可以获得结果:
The parent of `h1` is `body`.
打印的时候使用了 upgrade() 方法来尝试获取父节点的强引用,如果父节点已经被释放了,那么 upgrade() 会返回 None,否则会返回一个 Rc<Node> 的强引用。通过这种方式,我们可以安全地访问父节点,而不会导致内存泄漏或悬垂引用。故访问 Weak<T> 时需要先调用 upgrade() 方法来获取强引用,如果 upgrade() 返回 None,则说明数据已经被释放了。
自动迭代时使用的 Rc::downgrade() 方法将当前节点的强引用转换为弱引用(Weak<T>),并将其设置为子节点的父节点。这样就建立了父子关系,同时避免了循环引用的问题。
Mutex<T>
顾名思义,这是个互斥锁。它的核心作用是:在多线程环境下,保证同一时刻只有一个线程可以访问某个数据,从而安全地实现“修改”操作。
Mutex<T> 保证了在多线程时只能有一个线程能修改数据。但是其在真实线程中被移动时会发生编译错误 the trait Send is not implemented for Mutex<T> (rustc E0277),这是因为 Mutex<T> 本身并不实现 Send trait,所以无法在多个线程之间安全地传递。要解决这个问题,可以使用 Arc<Mutex<T>> 来包装 Mutex<T>,这样就可以在多个线程之间共享 Mutex<T> 的所有权,并且保证线程安全。
use std::{sync::{Arc, Mutex}, thread};
fn main() {
let way = Arc::new(Mutex::new(vec![vec![0; 256]; 256]));
let mut handles: Vec<thread::JoinHandle<()>> = vec![];
for t in 0..256 {
let way_clone = way.clone();
let handle = thread::spawn(move || {
for i in 0..256 {
way_clone.lock().unwrap()[t][i] = i + t;
}
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("{:?}", way.lock().unwrap());
}
这里在线程里面使用 lock() 方法来获取 Mutex<T> 的锁,获取锁成功后就可以安全地访问和修改数据了。访问完成后,锁会自动释放,以便其他线程能够获取锁并访问数据。
这样一来线程间竞态条件就被避免了,因为 Mutex<T> 会确保同一时刻只有一个线程能够访问数据。每当一个线程需要访问数据时,它必须先获取锁,获取锁成功后才能访问数据,访问完成后必须释放锁,以便其他线程能够获取锁并访问数据。
有的人可能知道一种东西叫原子类型,这个东西也是用来在多线程环境下保证数据安全的,但是它与
Mutex<T>不同的是,原子类型通过原子操作来实现线程安全,而不需要使用锁。这使得原子类型在某些情况下比Mutex<T>更高效,但它也有一些限制,因为原子类型把自己放在寄存器里面,如果写入数据量过大,性能不升反降。所以使用原子类型的数据一般是一些简单的数值类型,例如整数和布尔值,而Mutex<T>可以用于更复杂的数据结构。
RwLock<T>
RwLock<T> 是 Mutex<T> 的“升级进化版”。在 Rust 中,它同样用来包裹数据并提供内部可变性,但它的核心核心作用是:优化“多读少写”场景下的并发性能。
这个锁的建立依赖分治的 read() 读锁和 write() 写锁方法,在只读时不会阻塞其它读者,但是一旦出现写锁,其它线程既不能获取读锁也不能获取写锁,直到写锁被释放。这使得 RwLock<T> 在读多写少的场景下性能更优,因为它允许多个线程同时读取数据,而不需要等待写锁的释放。
即使如此,Mutex<T> 依然不能被废弃,因为在大量读情况下,写锁会陷入等待,如果读锁迟迟不释放并且后续继续大量排队读,写锁将会永远得不到锁,此现象被称作“写饥饿”,因此在写多读少的场景下,Mutex<T> 可能会表现得更好。
现在用一个较长的布尔向量来样式这个指针。初始化后选 10 个随机位置设置为 true,其他位置保持 false,之后来检查这个向量的状态:
use std::{sync::{RwLock, Arc}, thread};
use rand::RngExt;
let a = Arc::new(RwLock::new(vec![false; 4096]));
let mut rng = rand::rng();
for _ in 0..10 {
a.write().unwrap()[rng.random_range(0..4096)] = true;
}
上面这个地方用了 write() 方法来获取写锁,并且使用 unwrap() 来处理可能的错误。获取写锁成功后,就可以安全地修改数据了。
然后通过建立几个线程来检查这个向量里面是否有 true:
let mut handles = vec![];
for i in 0..8 {
let a_clone = a.clone();
let handle = thread::spawn(move || {
for j in 0..512 {
if a_clone.read().unwrap()[512 * i + j] {
a_clone.write().unwrap()[512 * i + j] = false;
}
}
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
最后检查这个向量里面是否还有 true:
// Check
a.read().unwrap().iter().for_each(|b| {
assert!(!*b);
});
println!("All clear!");
得到了 All clear! 的输出,说明所有的 true 都被成功地设置回 false 了。
Pin<T>
简单来说,Pin<T> 的核心作用就是:把一个对象死死地“钉”在内存里的某个固定位置,绝对不允许它被移动。
这时候有人就要问了:我没事钉一个东西是何意味?
其实不然,在 Rust 中,普通的变量默认都是可以被安全移动的。移动的本质,就是把一块内存数据复制到另一个新的内存地址上,然后让旧地址失效。但是有个东西叫做自引用类型,它的内部结构可能包含一个指向自己内部数据的指针,如果这个类型被移动了,那么这个指针就会变成悬垂指针,导致程序崩溃。
struct MyStruct {
data: String,
// ptr 指向了同一个结构体内部的 data 字段的内存地址
ptr: *const String,
}
这个结构体一旦被移动了,那么 ptr 就会指向一个无效的内存地址,导致悬垂指针问题。为了避免这种情况,Rust 提供了 Pin<T>,它可以确保被钉住的对象永远不会被移动,从而保证了自引用类型的安全性。
还有人要问了:我为什么要写出一个自引用的结构体,有用?
自引用类型在某些场景下非常有用,例如在异步编程中,Future 类型就是一个自引用类型,因为它需要在内部存储一些状态,并且在每次被轮询时都需要访问这些状态。
比如有一个异步函数:
async fn my_async_fn() {
let mut text = String::from("Hello");
let text_ref = &text;
some_other_async_fn().await;
println!("{}", text_ref);
}
当代码执行到 .await 暂停时,Rust 必须把当前函数的所有现场变量打包保存到一个隐式的结构体中(也就是包装成 Future),以便稍后恢复执行。在这个被打包的结构体里,text_ref 存储的正是 text 的地址,它自动变成了一个自引用结构体!
如果这个 Future 在被 await 的过程中在内存里被移动了,那等待你的就是内存灾难。因此,在异步任务开始执行后,它绝对不能在内存中被移动。
对于一些完全不需要 Pin<T> 的场景,Rust 也提供了 Unpin trait 来标记这些类型是可以被移动的。大部分类型都是 Unpin 的,例如基本类型、普通结构体和枚举等。当手动使用 Pin<T> 时,如果类型实现了 Unpin trait,那么这个 Pin<T> 不会生效。
其实一般的开发用不上 Pin<T>,因为 Rust 的编译器会自动处理大部分的移动问题。但是在一些高级场景下,例如异步编程,Pin<T> 就显得非常重要了。
比如,下面手动实现一个极其简单的 Future,来看看 Pin<T> 的使用:
struct MyFuture {
polled: bool,
}
这个是我的 Future 结构体,它有一个 polled 字段来记录是否被轮询过。实现一下 Future trait:
impl Future for MyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
if this.polled {
Poll::Ready(42)
} else {
this.polled = true;
println!("[poll] 还没准备好,返回 Pending...");
Poll::Pending
}
}
}
在这个实现中,poll 方法的第一个参数是 Pin<&mut Self>,这意味着在 poll 方法被调用时,MyFuture 的实例已经被钉住了,绝对不能被移动了。通过 self.get_mut() 获取到 MyFuture 的可变引用来访问和修改它的字段。
fn dummy_raw_waker() -> RawWaker {
fn clone(_: *const ()) -> RawWaker {
dummy_raw_waker()
}
fn wake(_: *const ()) {}
fn wake_by_ref(_: *const ()) {}
fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(std::ptr::null(), &VTABLE)
}
这个函数用来创建一个假的 RawWaker,它的 clone 方法会返回一个新的 RawWaker,而 wake、wake_by_ref 和 drop 方法则什么都不做。这个 RawWaker 可以用来创建一个 Context,从而测试我们的 MyFuture。
fn main() {
let future = MyFuture { polled: false };
let mut pinned = Box::pin(future);
let waker = unsafe { Waker::from_raw(dummy_raw_waker()) };
let mut cx = &mut Context::from_waker(&waker);
println!("第 1 次 poll:");
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Pending);
println!("第 2 次 poll:");
assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Ready(42));
println!("完成!");
}
调用第一次时这个 Future 还没有准备好,所以返回 Poll::Pending,第二次调用时它已经准备好了,所以返回 Poll::Ready(42)。
第 1 次 poll:
[poll] 还没准备好,返回 Pending...
第 2 次 poll:
完成!
以下是特定场景下的智能指针,主要用于实现一些特殊的功能,例如在不安全代码中使用。
UnsafeCell<T>
这个东西太底层,以至于平时用不到,但是有时候用这个把 &T 偷偷转换成 &mut T,就可以在不可变引用的情况下修改数据了。
这时候纯爱党表示不行啊,Rust 不是说了不可变引用不能修改数据吗?这不就是违反了 Rust 的所有权和借用规则吗?
信奉这套哲学的 Rust 语言设计者们也知道这个问题,所以他们在 UnsafeCell<T> 的文档里明确指出了:UnsafeCell<T> 是唯一一个允许在不可变引用的情况下修改数据的类型。但是,这个类型是非常危险的,如果使用不当,可能会导致数据竞争、悬垂指针等问题。因此,UnsafeCell<T> 只能在非常特殊的场景下使用,例如实现内部可变性或者在不安全代码中使用。
其实 Cell<T> 和 RefCell<T> 就是基于 UnsafeCell<T> 实现的,它们提供了更安全的接口来实现内部可变性,而不需要直接使用 UnsafeCell<T>。
impl<T> UnsafeCell<T> {
pub const fn get(&self) -> *mut T { ... }
}
这个东西的核心方法是 get(),它返回一个指向内部数据的裸指针。
极致优化性能的一批人会很喜欢…
use std::cell::UnsafeCell;
fn main() {
let data = UnsafeCell::new(42);
let ptr = &data; // Signed type `&UnsafeCell<i32>`
let raw_ptr = ptr.get(); // Signed type `*mut i32`
unsafe {
*raw_ptr = 114514;
}
unsafe {
println!("data = {:?}", *data.get());
}
}
这就改内部数据了:
data = 114514
ManuallyDrop<T>
众所周知,Rust 的所有权系统会在变量超出作用域时自动调用 drop 函数来释放资源。但是有时候我们可能需要手动控制资源的释放时机,这时候就可以使用 ManuallyDrop<T>。ManuallyDrop<T> 是一个智能指针,它允许我们手动调用 drop 函数来释放资源,而不是在变量超出作用域时自动调用。
比如说,C 语言里面常见(其实现在也不太常见了)的联合体(union),它可以在同一内存位置存储不同类型的数据,Rust 中联合体是默认不安全的。
这里不讨论联合体的事情,试试在与 C 语言交互时,使用 ManuallyDrop<T> 来手动控制资源的释放时机。
unsafe extern "C" {
fn create_player(name: *const u8, score: i32) -> *mut std::ffi::c_void;
fn print_player(p: *mut std::ffi::c_void);
fn free_player(p: *mut std::ffi::c_void);
}
在 Rust 头部先写一个 unsafe 块把 C 中的函数声明,如上。注意其声明方式遵循 Rust 规范,而非 C 规范,通过 std::ffi 链接 C 中特定类型。
C 文件中如此声明:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[64];
int score;
} Player;
Player* create_player(const char* name, int score) {
Player* p = (Player*)malloc(sizeof(Player));
strncpy_s(p->name, sizeof(p->name), name, _TRUNCATE);
p->score = score;
return p;
}
void print_player(const Player* p) {
printf("Player: %s, Score: %d\n", p->name, p->score);
}
void free_player(Player* p) {
free(p);
printf("freed\n");
}
这个程序对 Player 结构体进行创建、打印和释放操作。
struct CPlayer {
ptr: ManuallyDrop<*mut std::ffi::c_void>,
}
impl CPlayer {
fn new(ptr: *mut std::ffi::c_void) -> Self {
CPlayer {
ptr: ManuallyDrop::new(ptr),
}
}
fn into_raw(mut self) -> *mut std::ffi::c_void {
let p = unsafe { ManuallyDrop::take(&mut self.ptr) };
std::mem::forget(self);
p
}
}
此处建立了一个 CPlayer 结构体来包装 C 中的 Player 结构体的指针,并且使用 ManuallyDrop<T> 来手动控制资源的释放时机。into_raw() 方法会返回一个裸指针,并且使用 ManuallyDrop::take() 来获取内部数据,同时使用 std::mem::forget() 来防止 Rust 自动调用 drop 函数来释放资源。
接下来手动实现 Drop trait 来在 CPlayer 超出作用域时调用 C 中的 free_player() 函数来释放资源:
impl Drop for CPlayer {
fn drop(&mut self) {
unsafe { free_player(*self.ptr) };
}
}
下面,调用 C 中的函数来创建和打印玩家,并且在 CPlayer 超出作用域时自动释放资源。这时能发现,传入的字符串必须以 \0 结尾,因为 C 语言中的字符串是以空字符结尾的,as_ptr() 方法还提供了字符串的内存指向,方便 C 使用。
fn main() {
let ptr = unsafe { create_player("Shizuku\0".as_ptr(), 9999) };
let player = CPlayer::new(ptr);
unsafe { print_player(*player.ptr) };
let ptr2 = unsafe { create_player("Rin\0".as_ptr(), 7777) };
let player2 = CPlayer::new(ptr2);
unsafe { print_player(*player2.ptr) };
let raw = player2.into_raw();
unsafe { free_player(raw) };
}
输出:
Player: Shizuku, Score: 9999
Player: Rin, Score: 7777
freed
freed
这样 C 语言程序就能与 Rust 交互了,并且通过 ManuallyDrop<T> 来手动控制资源的释放时机,避免了 Rust 自动调用 drop 函数来释放资源,从而实现了更灵活的资源管理。
整个 Rust 程序内,发现并没有引入 C 文件,那么 Rust 是如何知道 C 文件的存在的呢?其实是通过 build.rs 文件来实现的。build.rs 是一个构建脚本,它会在编译时被执行,可以用来生成代码、编译 C/C++ 代码等。
下面是 build.rs 文件的内容:
fn main() {
cc::Build::new()
.file("examples/ffi_helper.c")
.compile("ffi_helper");
}
注意,如果不在 build.rs 文件中指定 C 文件的路径,那么 Rust 是无法知道 C 文件的存在的。
PhantomData<T>
它在运行时完全不占用任何内存空间,但它能凭空制造出一个“幻影”,在编译阶段强行改变编译器对所有权、生命周期以及线程安全的判断。
这个指针的核心作用就是在编译期可以强行控制生命周期约束、所有权 Drop 检查、以及 Send/Sync 线程安全标记。简单来说就是个绑定 Marker。
比如在嵌入式工作中,常要一个状态机,这个状态机接纳状态,但是又不使用状态的值,这时候就可以使用 PhantomData<T> 来告诉编译器这个状态机是依赖于某个类型的,从而让编译器在编译时进行正确的检查。
现有一投放工作的高炉,具有待机和运行两个状态。
// 高炉状态
struct Idling; // 在待机状态
struct Running; // 在运行状态
定义一个 BlastFurnace 结构体来表示高炉,并且使用 PhantomData<T> 来绑定状态类型:
// 高炉实例
struct BlastFurnace<State> {
temperature: f64,
_status: PhantomData<State>
}
实现方法以支持高炉启停与对象创建:
impl BlastFurnace<Idling> {
fn start(&self) -> BlastFurnace<Running> {
println!("高炉启动,核心温度 {} ℃", self.temperature);
BlastFurnace {
temperature: self.temperature + 1145.14,
_status: PhantomData
}
}
fn new() -> Self {
BlastFurnace {
temperature: ENV_TEMPERATURE,
_status: PhantomData
}
}
}
impl BlastFurnace<Running> {
fn stop(&mut self) -> BlastFurnace<Idling> {
println!("高炉停止,最终核心温度 {} ℃", self.temperature);
self.temperature = ENV_TEMPERATURE;
BlastFurnace {
temperature: self.temperature,
_status: PhantomData
}
}
}
注意,在状态机操作必须返回操作后对应状态的实例,这样才能保证状态转换的正确性。
接下来试着使用这个状态机:
fn main() {
let bf = BlastFurnace::new(); // Idling
let mut bf = bf.start(); // Running
#[allow(unused)]
let bf = bf.stop(); // Idling
}
输出:
高炉启动,核心温度 14.5 ℃
高炉停止,最终核心温度 1159.64 ℃
这样的设计理念有助于建立一个安全状态机,避免了在 Running 状态下调用 stop() 方法时出现编译错误,因为 stop() 方法只能在 Running 状态下被调用。同样地,在 Idling 状态下调用 start() 方法时也会出现编译错误,因为 start() 方法只能在 Idling 状态下被调用。通过这种方式,我们可以确保状态转换的正确性,并且在编译时就能发现潜在的错误。
以上就是 Rust 中的部分智能指针了,虽然它们的名字都叫“指针”,但它们的功能和用途却各不相同。还有一些其它智能指针,如有需要则做响应补充。
如果本文有误,请多加指正!
