Rust 中的智能指针:是什么、为什么以及如何?

2025-06-10

Rust 中的智能指针:是什么、为什么以及如何?

TL;DR:介绍一些 Rust 的智能指针Box,,,,,CellRefCellRcArcRwLockMutex

智能指针显然是……智能的指针。但这个“智能”究竟是什么意思呢?什么时候应该使用它们?它们是如何工作的?

这些就是我将在这里开始解答的问题。这只是一个答案的开端,仅此而已。我希望本文能为您提供“理解背景”(例如对概念的“熟悉”),帮助您真正理解这个主题,而这需要阅读官方文档,当然,也需要实践。

如果你已经熟悉了,可以把这篇文章作为相关阅读材料的清单。请查看每节开头的“有用链接”。

指数:

  1. 盒子
  2. 细胞
  3. 参考细胞
  4. 钢筋混凝土
  5. 读写锁
  6. 互斥锁

智能点一般

正如《Rust 之书》中所解释的,指针是包含“指向”其他数据的地址的变量。Rust 中常见的指针是引用 ( &)。智能指针是“具有额外元数据和功能”的指针,例如,它们可以计算值被借用的次数,提供管理读写锁的方法等等。

从技术上讲,StringVec也是智能指针,但我不会在这里介绍它们,因为它们很常见并且通常被认为是类型而不是指针。

还要注意,在此列表中,只有ArcRwLockMutex是线程安全的。


Box<T>

有用的链接:书籍文档盒子、堆栈和堆

什么?

Box<T>允许你存储T在堆上。所以,如果你有一个本来u64应该存储在栈上的变量,Box<u64>它会被存储在堆上。

如果您对堆栈和堆的概念不太熟悉,请阅读此文

为什么?

存储在栈中的值不能增长,因为 Rust 需要在编译时知道它的大小。据我所知,关于这会如何影响你的编程的最好例子是递归》请考虑下面的代码(及其注释)。

// This does not compile. The List contains itself,  
// being recursive and therefore having an infinite size.
enum List { 
   Cons(i32, List),
   Nil,
}
// This does compile because the size of a pointer
// does not change according to the size of the pointed value.
enum List { 
   Cons(i32, Box<List>),
   Nil,
}
Enter fullscreen mode Exit fullscreen mode

请务必阅读本书的此部分以了解详细信息。

更一般地说,Box当你的值太大而无法保存在堆栈中或者你需要拥有它时,它很有用。

如何?

T要获取其中的值,Box<T>你只需取消引用它。

let boxed = Box::new(11);
assert_eq!(*boxed, 11)
Enter fullscreen mode Exit fullscreen mode

单元格<T>

有用的链接:模块文档指针文档

什么?

Cell<T>提供对 的共享引用,T同时允许您更改T。这是 模块提供的“可共享可变容器”之一std::cell

为什么?

在 Rust 中,共享引用是不可变的。这保证了当你访问内部值时,不会得到与预期不同的结果,同时也确保了你不会在它被释放后尝试访问它(这在 70% 的内存安全漏洞中占了很大一部分)。

如何?

Cell<T>提供了控制我们对 的访问的函数。你可以在这里T找到它们,但为了便于解释,我们只需要两个:get()set()

基本上,Cell<T>允许你自由地使用 进行修改,T因为T.set()当你使用 时T.get(),你获取Copy的是 的T,而不是引用。这样,即使你更改T,你使用 获得的复制值get()也将保持不变;如果你销毁T,也不会有任何指针悬空。

最后要注意的是,这T必须实现。Copy

use std::cell::Cell;
let container = Cell::new(11);
let eleven = container.get();
{
    container.set(12);
}
let twelve = container.get();
assert_eq!(eleven, 11);
assert_eq!(twelve, 12);
Enter fullscreen mode Exit fullscreen mode

RefCell<T>

有用的链接:本书文档

什么?

RefCell<T>也给出了对的共享引用T,但是虽然Cell是静态检查的(Rust 在编译时检查它),但却RefCell<T>是动态检查的(Rust 在运行时检查它)。

为什么?

因为Cell使用副本进行操作,所以您应该限制自己使用较小的值,这意味着您再次需要引用,这会让我们回到Cell解决的问题。

处理这个问题的方法RefCell是跟踪谁在读,谁在写T。这就是为什么RefCell<T>要动态检查:因为要编写这个检查代码。不过不用担心,Rust 仍然会确保你在编译时不会搞砸。

如何?

RefCell<T>有一些方法可以借用 的可变或不可变引用T;如果操作不安全,则不允许你这样做。与 一样Cell, 中也有一些方法RefCell,但这两个方法足以说明这个概念:borrow(),它获取一个不可变引用;borrow_mut(),它获取一个可变引用。 使用的逻辑RefCell大致如下:

  • 如果没有对的引用(无论是可变的还是不可变的)T,您可能会获得对它的可变或不可变的引用;
  • 如果已经有一个对可变引用T,则您可能什么也得不到,而必须等到该引用被删除;
  • 如果有一个或多个对不可变引用T,您可能会获得对它的不可变引用。

正如您所见,没有办法T同时获取可变和不可变引用。

记住:不是线程安全的。我说的“不可能”指的是单线程。

另一种思考方式是:

  • 不可变引用是共享引用;
  • 可变引用是唯一引用。

值得一提的是,上面提到的函数有一些变体不会惊慌,而是返回Resulttry_borrow()try_borrow_mut()

use std::cell::RefCell;
let container = RefCell::new(11);
{
    let _c = container.borrow();
    // You may borrow as immutable as many times as you want,...
    assert!(container.try_borrow().is_ok());
    // ...but cannot borrow as mutable because 
    // it is already borrowed as immutable.
    assert!(container.try_borrow_mut().is_err());
} 
// After the first borrow as mutable...
let _c = container.borrow_mut();
// ...you cannot borrow in any way.
assert!(container.try_borrow().is_err());
assert!(container.try_borrow_mut().is_err());
Enter fullscreen mode Exit fullscreen mode

Rc<T>

有用的链接:本书模块文档指针文档Rust 示例

什么?

我将引用此文档:

该类型提供对堆上分配的Rc<T>类型为 的值的共享所有权。调用on会产生一个指向堆上相同分配空间的新指针。当指向给定分配空间的最后一个指针被销毁时,存储在该分配空间中的值(通常称为“内部值”)也会被丢弃。TcloneRcRc

因此,就像 一样Box<T>在堆上Rc<T>分配。区别在于,克隆会在另一个 内部生成另一个,而克隆会将另一个 克隆同一个TBox<T>TBoxRc<T>Rc T

另一个重要的评论是,我们在 中没有Rc像在Cell或 中那样的内部可变性RefCell

为什么?

您希望对某些值进行共享访问(而不复制它),但您希望在不再使用它时(即当没有对它的引用时)释放它。

由于 没有内部可变性Rc,因此通常将其与Cell或一起使用RefCell,例如Rc<Cell<T>>

如何?

使用Rc<T>,您正在使用clone()方法。在后台,它会计算您拥有的引用数,当它变为零时,它会减少T

use std::rc::Rc;
let mut c = Rc::new(11);

{ 
    // After borrwing as immutable...
    let _first = c.clone();

    // ...you can no longer borrow as mutable,...
    assert_eq!(Rc::get_mut(&mut c), None);

    // ...but can still borrow as immutable.
    let _second = c.clone();

    // Here we have 3 pointer ("c", "_first" and "_second").
    assert_eq!(Rc::strong_count(&c), 3);
}

// After we drop the last two, we remain only with "c" itself.
assert_eq!(Rc::strong_count(&c), 1);

// And now we can borrow it as mutable.
let z = Rc::get_mut(&mut c).unwrap();
*z += 1;
assert_eq!(*c, 12);
Enter fullscreen mode Exit fullscreen mode

Arc<T>

有用的链接:文档Rust 示例

什么?

Arc是的线程安全版本Rc,因为它的计数器是通过原子操作进行管理的。

为什么?

我认为使用Arc而不是 的原因Rc很明显(线程安全),所以相关的问题变成了:为什么不Arc 每次都使用?答案是 提供的这些额外控制Arc会带来开销。

如何?

就像RcArc<T>您将使用clone()来获取指向相同值的指针T,一旦最后一个指针被删除,该指针就会被销毁。

use std::sync::Arc;
use std::thread;

let val = Arc::new(0);
for i in 0..10 {
    let val = Arc::clone(&val);

    // You could not do this with "Rc"
    thread::spawn(move || {
        println!(
            "Value: {:?} / Active pointers: {}", 
            *val+i, 
            Arc::strong_count(&val)
        );
    });
}
Enter fullscreen mode Exit fullscreen mode

RwLock<T>

有用的链接:文档

RwLock也由板条箱提供parking_lot

什么?

作为读写锁,只有持有以下锁之一时RwLock<T>才会授予访问权限:,遵循以下规则:Treadwrite

  • :如果你想要一个读锁,只要没有写者持有该锁,你就可以得到它;否则,你必须等到它被释放;
  • 写入:如果您想要一个写入锁,只要没有人(读者或写入者)持有锁,您就可以获得该锁;否则,您必须等到他们被放弃;

为什么?

RwLock允许你从多个线程读写相同的数据。与Mutex(见下文)不同,它区分锁的类型,因此read,只要你没有write锁,就可以拥有多个锁。

如何?

当你想要读取 a 时RwLock,你必须使用函数read()—or try_read()—,它会返回一个LockResult包含 a 的 a RwLockReadGuard。如果读取成功,你就可以RwLockReadGuard通过 deref 访问其中的值。如果写入者持有锁,则线程将被阻塞,直到它能够获取锁为止。

当你尝试使用write()—or时也会发生类似的情况try_write()。不同之处在于,它不仅会等待持有锁的写入者,还会等待任何持有锁的读取者。

use std::sync::RwLock;
let lock = RwLock::new(11);

{
    let _r1 = lock.read().unwrap();
    // You may pile as many read locks as you want.
    assert!(lock.try_read().is_ok());
    // But you cannot write.
    assert!(lock.try_write().is_err());
    // Note that if you use "write()" instead of "try_write()"
    // it will wait until all the other locks are released
    // (in this case, never).
} 

// If you grab the write lock, you may easily change it
let mut l = lock.write().unwrap();
*l += 1;
assert_eq!(*l, 12);
Enter fullscreen mode Exit fullscreen mode

如果某个持有锁的线程崩溃了,进一步尝试获取锁将返回PoisonError,这意味着从那时起每次尝试读取RwLock都将返回相同的。您可以使用PoisonError来从中毒中恢复RwLockinto_inner()

use std::sync::{Arc, RwLock};
use std::thread;

let lock = Arc::new(RwLock::new(11));
let c_lock = Arc::clone(&lock);

let _ = thread::spawn(move || {
    let _lock = c_lock.write().unwrap();
    panic!(); // the lock gets poisoned
}).join();

let read = match lock.read(){
    Ok(l) => *l,
    Err(poisoned) => {
        let r = poisoned.into_inner();
        *r + 1
    }
};

// It will be 12 because it was recovered from the poisoned lock
assert_eq!(read,12);
Enter fullscreen mode Exit fullscreen mode

互斥锁

有用的链接:本书文档

Mutex也由板条箱提供parking_lot

什么?

Mutex与 类似RwLock,但它只允许一个锁持有者,无论它是读者还是作者。

为什么?

Mutex优于的一个原因RwLockRwLock可能会导致写入器饥饿(当读者蜂拥而至并且写入器永远没有机会获得锁时,永远等待),而 则不会发生这种情况Mutex

当然,我们在这里深入探讨,因此现实生活中的选择取决于更高级的考虑,例如您期望同时有多少个读者,操作系统如何实现锁,等等......

如何?

Mutex和 的RwLock工作方式类似,不同之处在于,由于Mutex不区分读者和作者,因此您只需使用lock()try_lock即可获取MutexGuard。 中毒逻辑也发生在这里。

use std::sync::Mutex;
let guard = Mutex::new(11);

let mut lock = guard.lock().unwrap();
// It does not matter if you are locking the Mutex to read or write,
// you can only lock it once.
assert!(guard.try_lock().is_err());

// You may change it just like you did with RwLock
*lock += 1;
assert_eq!(*lock, 12);
Enter fullscreen mode Exit fullscreen mode

您可以采用与Mutex处理中毒相同的方法来处理中毒RwLock


感谢您的阅读!

鏂囩珷鏉ユ簮锛�https://dev.to/rogertorres/smart-pointers-in-rust-what-why-and-how-oma
PREV
一个小家伙是如何成为开源贡献者的?
NEXT
[Rust] Tokio 堆栈概览:运行时