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

977 阅读10分钟

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

img

TL;DR: 我将介绍一些 Rust 的智能指针: Box, Cell, RefCell, Rc, Arc, RwLockMutex.

很明显,智能指针是... 指针 smart 的 。 但是 什么 这个“聪明”究竟是 意思呢? 时候 我们什么 应该使用它们? 如何 它们是 工作的?

这些是我将在这里开始回答的问题。 就是这样: 答案的开始 ,仅此而已。 我希望这篇文章能给你一个“理解背景”(类似于“熟悉”这个概念),这将帮助你真正理解这个主题,这将来自阅读官方文档,当然还有实践.

如果您已经熟悉它,您可以将本文用作相关阅读的列表。 在每个部分的开头寻找“有用的链接”。

指数:

  1. [盒子 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#box)
  2. [细胞 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#cell)
  3. [参考单元格 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#refcell)
  4. [RC ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#rc)
  5. [弧 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#arc)
  6. [锁 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#rwlock)
  7. [互斥体 ](file:///D:/桌面/Smart_Pointers_in_Rust _What,_why_and_how .html#mutex)

一般智能点

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

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

另请注意,从该列表中,只有 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>允许你自由改变 TT.set()因为当你使用 T.get(),你检索 CopyT,不做参考。 这样,即使你改变 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,分配在堆上。 调用 cloneRc在堆上产生一个指向相同分配的新指针。 当最后 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 ); 

有用的链接: 文档 Rust 的例子

什么?

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.

当然,我们这里是潜入更深的海域,所以现实生活中的选择取决于更高级的考虑,比如你期望同时有多少读者,操作系统如何实现锁,等等……

如何?

MutexRwLock以类似的方式工作,不同之处在于,因为 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.

感谢您的阅读!