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

TL;DR: 我将介绍一些 Rust 的智能指针:
Box,Cell,RefCell,Rc,Arc,RwLock和Mutex.
很明显,智能指针是... 指针 smart 的 。 但是 什么 这个“聪明”究竟是 意思呢? 时候 我们什么 应该使用它们? 如何 它们是 工作的?
这些是我将在这里开始回答的问题。 就是这样: 答案的开始 ,仅此而已。 我希望这篇文章能给你一个“理解背景”(类似于“熟悉”这个概念),这将帮助你真正理解这个主题,这将来自阅读官方文档,当然还有实践.
如果您已经熟悉它,您可以将本文用作相关阅读的列表。 在每个部分的开头寻找“有用的链接”。
指数:
- [盒子 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#box)
- [细胞 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#cell)
- [参考单元格 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#refcell)
- [RC ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#rc)
- [弧 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#arc)
- [锁 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#rwlock)
- [互斥体 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#mutex)
一般智能点
正如 所解释 The Book 中 的 ,指针是包含“指向”一些其他数据的地址的变量。 Rust 中通常的指针是引用 ( &)。 智能指针是“具有附加元数据和功能”的指针,例如,它们可以计算值被借用的次数,提供管理读写锁的方法等。
从技术上讲, String和 Vec也是智能指针,但我不会在这里介绍它们,因为它们很常见并且通常被认为是类型而不是指针。
另请注意,从该列表中,只有 Arc, RwLock, 和 Mutex是线程安全的。
盒子
什么?
Box<T>允许你存储 T在堆上。 所以,如果你有,比如说,一个 u64这将存储在堆栈中, Box<u64>将它存储在堆上。
如果您对堆栈和堆的概念不满意,请阅读 此 。
为什么?
存储在堆栈中的值不能增长,因为 Rust 需要在编译时知道它的大小。 我所知道的关于这可能如何影响您的编程的最佳示例是 The Book : recursion 。 考虑下面的代码(及其注释)。
// 这不会编译。 该列表包含自身,
// 是递归的,因此具有无限大小。
枚举 列表 {
缺点 ( i32 , 列表 ),
无 ,
}
// 这确实可以编译,因为指针的大小
// 不会根据指向值的大小而改变。
枚举 列表 {
缺点 ( i32 , Box < List > ),
无 ,
}
请务必阅读 这一部分 The Book 上的 以了解详细信息。
更笼统地说, Box当您的价值太大而无法保留在堆栈中或您需要 时, 拥有 它 这很有用。
如何?
获取价值 T里面 Box<T>你只需要取消它。
让 盒装 = Box :: new ( 11 );
assert_eq! ( * 盒装 , 11 )
细胞
什么?
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以及。
使用 std :: cell :: Cell ;
让 容器 = Cell :: new ( 11 );
让 11 = 容器 。 得到 ();
{
容器 。 设置 ( 12 );
}
让 十二 = 容器 。 得到 ();
assert_eq! ( 十一 , 11 );
assert_eq! ( 十二 、 十二 );
参考单元格
什么?
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同时。
请记住: 这 不是 线程安全的。 当我说“不可能”时,我指的是单个线程。
另一种思考方式是:
- 不可变 引用是 共享 引用;
- 可变 引用是 独占 引用。
值得一提的是,上面提到的函数都有变种,不会panic,但是会返回 Result反而: try_borrow()和 try_borrow_mut();
使用 std :: cell :: RefCell ;
让 容器 = RefCell :: new ( 11 );
{
让 _c = 容器 。 借 ();
// 您可以根据需要多次不可变地借用,...
断言! ( 容器 。 try_borrow ()。 is_ok ());
// ...但不能借为可变的,因为
// 它已经被借用为不可变的。
断言! ( 容器 。 try_borrow_mut ()。 is_err ());
}
// 在第一次作为可变借用之后...
让 _c = 容器 。 借_mut ();
// ...你不能以任何方式借用。
断言! ( 容器 。 try_borrow ()。 is_err ());
断言! ( 容器 。 try_borrow_mut ()。 is_err ());
RC
有用的链接: 本书 ; 模块文档 ; 指针文档 ; Rust 的例子 。
什么?
我将引用有关此文档的文档:
方式
Rc<T>提供类型值的共享所有权T,分配在堆上。 调用clone在Rc在堆上产生一个指向相同分配的新指针。 当最后Rc指向给定分配的指针被销毁,存储在该分配中的值(通常称为“内部值”)也被删除。
所以,就像一个 Box<T>, Rc<T>分配 T在堆上。 区别在于克隆 Box<T>会给你另一个 T在另一个里面 Box克隆时 Rc<T>给你另一个 Rc对 同 T.
另一个重要的评论是我们没有内部可变性 Rc正如我们在 Cell或者 RefCell.
为什么?
您希望共享对某个值的访问权(而不对其进行复制),但是一旦它不再使用,即没有对它的引用时,您又希望将其释放。
由于没有内部可变性 Rc,通常与它一起使用 Cell或者 RefCell, 例如, Rc<Cell<T>>.
如何?
和 Rc<T>,您正在使用 clone()方法。 在幕后,它会计算您拥有的引用数量,当它变为零时,它会下降 T.
使用 std :: rc :: Rc ;
让 mut c = Rc :: new ( 11 );
{
// 在借用为不可变之后...
让 _first = c 。 克隆 ();
// ...你不能再借为可变的,...
assert_eq! ( Rc :: get_mut (& mut c ), None );
// ...但仍然可以借用为不可变的。
让 _second = c 。 克隆 ();
// 这里我们有 3 个指针(“c”、“_first”和“_second”)。
assert_eq! ( Rc :: strong_count (& c ), 3 );
}
// 在我们去掉最后两个之后,我们只剩下“c”本身。
assert_eq! ( Rc :: strong_count (& c ), 1 );
// 现在我们可以借用它作为可变的。
让 z = Rc :: get_mut (& mut c )。 解开 ();
* z + = 1 ;
assert_eq! ( * c , 12 );
弧
什么?
Arc是线程安全的版本 Rc,因为它的计数器是通过原子操作管理的。
为什么?
我想你会使用的原因 Arc代替 Rc很清楚(线程安全),所以相关的问题变成:为什么不直接使用 Arc 每次 ? 答案是这些额外的控件由 Arc附带间接费用。
如何?
就像 Rc, 和 Arc<T>你将使用 clone()获取指向相同值的指针 T,一旦最后一个指针被删除,它将被销毁。
使用 std :: sync :: Arc ;
使用 std :: thread ;
让 val = Arc :: new ( 0 );
因为 我 在 0 .. 10 {
让 val = Arc :: clone (& val );
// 你不能用“Rc”来做到这一点
线程 :: spawn ( 移动 || {
打印! (
"值:{:?} / 活动指针:{}" ,
* val + i ,
弧 :: strong_count (& val )
);
});
}
锁
有用的链接: 文档 。
RwLock也是由parking_lot箱。
什么?
作为读写锁, RwLock<T>只会允许访问 T一旦你持有其中一把锁: read或者 write,根据这些规则给出:
- 读 :如果你想读一个锁,只要 ,你就可以得到它 没有写 者持有锁 ; 否则,你必须等到它被丢弃;
- 写 想写 :如果你 一个锁,你可能会得到,只要 没有人 ,读者或作者,持有锁; 否则,您必须等到它们被丢弃;
为什么?
RwLock允许您从多个线程读取和写入相同的数据。 不同于 Mutex(见下文),它区分了锁的种类,所以你可能有几个 read只要你没有锁 write锁。
如何?
当你想阅读一个 RwLock,你必须使用这个函数 read()-或者 try_read()——这将返回一个 LockResult包含一个 RwLockReadGuard. 如果成功,就可以访问里面的值了 RwLockReadGuard通过使用 deref。 如果作者持有锁,线程将被阻塞,直到它可以持有锁。
当您尝试使用时会发生类似的事情 write()-或者 try_write(). 不同之处在于它不仅会等待持有锁的写入者,还会等待持有锁的任何读取者。
使用 std :: sync :: RwLock ;
让 锁 = RwLock :: new ( 11 );
{
让 _r1 = 锁定 。 读 ()。 解开 ();
// 您可以根据需要堆积尽可能多的读锁。
断言! ( 锁 。 try_read ()。 is_ok ());
// 但是你不能写。
断言! ( 锁 。 try_write ()。 is_err ());
// 请注意,如果您使用“write()”而不是“try_write()”
// 它将等待直到所有其他锁被释放
//(在这种情况下,从不)。
}
// 如果你抢了写锁,你可以很容易地改变它
让 mut l = 锁定 。 写 ()。 解开 ();
* 1 + = 1 ;
assert_eq! ( * l , 12 );
如果某个持有锁的线程发生混乱,进一步尝试获取锁将返回一个 PoisonError,这意味着从那时起每次尝试阅读 RwLock将返回相同 PoisonError. 你可能会从中毒中恢复过来 RwLock使用 into_inner().
使用 std :: sync ::{ Arc , RwLock };
使用 std :: thread ;
让 锁 = Arc :: new ( RwLock :: new ( 11 ));
让 c_lock = Arc :: clone (& lock );
让 _ = 线程 :: spawn ( 移动 || {
让 _lock = c_lock 。 写 ()。 解开 ();
恐慌! (); // 锁中毒
})。 加入 ();
让 读 = 匹配 锁 。 读 (){
好的 ( l ) => * l ,
错误 ( 中毒 ) => {
让 r = 中毒 。 into_inner ();
* r + 1
}
};
// 将是 12,因为它是从中毒锁中恢复的
assert_eq! ( 阅读 , 12 );
互斥体
Mutex也是由parking_lot箱。
什么?
Mutex类似于 RwLock,但它只允许一个锁持有者,无论是读者还是作者。
为什么?
喜欢的理由之一 Mutex超过 RwLock就是它 RwLock可能会导致作家饥饿(当读者堆积如山而作家永远没有机会拿到锁,永远等待),这不会发生 Mutex.
当然,我们这里是潜入更深的海域,所以现实生活中的选择取决于更高级的考虑,比如你期望同时有多少读者,操作系统如何实现锁,等等……
如何?
Mutex和 RwLock以类似的方式工作,不同之处在于,因为 Mutex不区分读者和作者,您只需使用 lock()或者 try_lock得到 MutexGuard. 中毒逻辑也发生在这里。
使用 std :: sync :: Mutex ;
让 guard = Mutex :: new ( 11 );
让 mut lock = guard 。 锁 ()。 解开 ();
// 不管你是锁定互斥锁来读还是写,
// 你只能锁定一次。
断言! ( 后卫 。 try_lock ()。 is_err ());
// 您可以像使用 RwLock 一样更改它
* 锁定 += 1 ;
assert_eq! ( * 锁 , 12 );
你可以处理中毒 Mutex就像你处理中毒一样 RwLock.
感谢您的阅读!