9

Rust:智能指针

 3 years ago
source link: https://zhuanlan.zhihu.com/p/125770192
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust:智能指针

莫得灵魂的程序员

1、什么是智能指针

指针Pointer)是一个包含内存地址的变量的通用概念,这个地址引用,或"指向(Point At)"一些其他数据。Rust中最常见的指针是引用(Reference),引用以"&"符号为标记借用了他们所指向的值,除了引用数据没有任何其他特殊功能,无额外开销

智能指针Smart Pointer)是一类数据结构,他们不仅表现像指针,并且拥有额外的元数据与功能。智能指针起源于C++并存在于其他编程语言中。

2、Rust有哪些常用的智能指针

Rust标准库(std)中提供了多种不同的智能指针,这些指针除引用外都带有一些特殊的功能,包含但不局限于:StringVec<T>Box<T>Rc<T>Arc<T>Weak<T>Cell<T>RefCell<T>UnsafeCell<T>,特别的 Cell<T>,RefCell<T>是在UnsafeCell<T>基础之上构建的。

3、智能指针的实现基础

智能指针通常使用结构体实现,其区别于常规结构体的显著特性在于其实现了 DerefDrop 两种trait。

实现Deref trait允许我们重载解引用运算符(dereference operator)*(区别于乘法运算符、glob运算符)。通过这种方式实现Deref trait的智能指针可以被当作常规引用来对待,这样我们编写的操作引用的代码便能适用于智能指针,如下例子表明智能指针和普通引用可以一致对待

 fn main() {
     let s1 = &5;
     let s2 = Box::new(5);
 ​
     assert_eq!(5, *s1);
     assert_eq!(5, *s2);
 }

实际上上述例子在使用 *s2 的时候,Rust在底层运行时为 *(s2.deref()),这样一来我们便不需要手动调用deref方法了,这个特性让我们面对无论是普通引用还是实现了Deref的类型时都能写出一致的代码。 * 之所以是必须的,是因为deref返回的是值的引用而不是值(如果直接返回值,则值的所有权将被移出self,导致这个s2对5失去所有权)。

Deref紧密相关的另外一个Rust特性是:解引用强制多态(deref coercions),其将实现了 Deref 的类型的引用转换为原始类型通过 Deref 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 deref 方法被调用,把我们提供的类型转换成了参数所需的类型。比如如下列子,&s 会被解引用两次,第一次解引用变为&String(Box实现了Deref trait),第二次解引用变为&str(String实现了Deref trait)。

 fn demo(i: &str) {
     println!("{:?}", i);
 }
 fn main() {
     let s = Box::new(String::from("hello"));
     demo(&s);
 }

Deref实现的是不可变解引用,还有与之对应的是DerefMut实现的是可变解引用

实现Drop trait允许我们在值要离开作用于时执行一些代码,最常用的莫过于清理内存或资源,以便释放相关内存和资源。Drop trait要求实现一个叫drop的方法,它获取一个self的可变引用。

4、智能指针与所有权

普通引用只是一类只借用数据的指针,含不可变借用(&T)和可变借用(&mut T)。智能指针则拥有他们指向的数据,比如StringVec<T>,都拥有一些数据并允许修改这些它们,它们也带有元数据和额外的功能与保证。

在上述前提下,引出了Rust的一个设计模式:内部可变性(Interior mutability),它允许你在只拥有不可变引用时修改数据,这通常是借用规则所不允许的。RefCell<T>就是一种遵循内部可变性模式的类型,将在后续给出详细使用方式

5、智能指针的使用场景

5.1、String,实际定义是使用了Vec<u8>

 pub struct String {
     vec: Vec<u8>,
 }

5.2、Vec<T>:长度,容量

 pub struct Vec<T> {
     buf: RawVec<T>,
     len: usize,
 }

额外提供了两个特性:实际长度,分配容量,如

 fn main() {
     let mut v: Vec<i32> = vec![];
     v.reserve(50);
     v.push(1);
     println!("{:?}", v.capacity());
     println!("{:?}", v.len());
 }

5.3、Box<T>:数据放在堆上

最简单直接的智能指针是box,其类型是 Box<T>Box<T>允许你将值T存放在堆上而不是栈上,栈上则保留指向堆数据的指针,除此之外没有性能损失,但也没有提供额外的功能。

 fn main() {
     let x = Box::new(5);
     println!("x = {}", x);
 }

Box<T>类型经常用于解决如下三个类型的需求:

a. 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候,创建递归类型就是一个经典例子

如果我们模仿C语言,定义一个递归的结构体会如何?

 struct Node {
     data: i32,
     next: Node,
 }
 fn main() {
 }

当然是编译失败,会提示如下

 error[E0072]: recursive type `Node` has infinite size
  --> src\main.rs:6:1
   |
 6 | struct Node {
   | ^^^^^^^^^^^ recursive type has infinite size
 7 |     data: i32,
 8 |     next: Node,
   |     ---------- recursive without indirection
   |
   = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `Node` represent
 able

用从错误中的help也可以看到,Box可以解决这个问题,使用Box的递归的结构体如下:

 struct Node {
     data: i32,
     next: Box<Node>,
 }
 fn main() {
 }

这个可以编译通过,但是实际上无法使用,因为next一定要有值,所以你得定义一个新的Node给next,这个新定义的Node也得有一个next......没完没了,可以使用Option解决这个问题,完整Demo如下:

 #[derive(Debug)]
 struct Node {
     data: i32,
     next: Option<Box<Node>>,
 }
 fn main() {
     let node = Node {
         data: 2,
         next: None,
     };
     let root = Node {
         data: 1,
         next: Some(Box::new(node)),
     };
     println!("{:?}", root);
 }

b. 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的,保存在堆上的时候,转移所有权时只需要拷贝栈上的指针即可

c. 当希望拥有一个值并只关心它的类型是否实现了特定trait而不是其具体类型的时候,直接上代码(dyn Printable为trait 对象,可以用&dyn Printable 或者Box<dyn Printable>)

 trait Printable {
     fn stringify(&self) -> String;
 }
 impl Printable for i32 {
     fn stringify(&self) -> String { self.to_string() }
 }
 fn print(a: Box<dyn Printable>) {
     println!("{}", a.stringify());
 }
 fn main() {
     print(Box::new(10) as Box<dyn Printable>);
 }

5.4、Rc<T>,Weak<T>:多所有权

在很多情况下,我们可以很准确的知道哪个变量拥有某个值,但是在有些情况下,一个变量可能会有多个所有者(e.g. 在图数据结构中,多个边可能指向相同的终点,这个节点从概念上讲为所有指向它的边所拥有),Rust提供了 Rc<T>类型来满足多所有权的需要,其名称为引用计数(reference counting)的缩写Rc

 fn main() {
     let toy = Rc::new(String::from("toy car"));
     let brother = Child {
         name: String::from("brother"),
         toy: Rc::clone(&toy),
     };
     let sister = Child {
         name: String::from("sister"),
         toy: Rc::clone(&toy),
     };
     println!("{:?}", brother);  // Child { name: "brother", toy: "toy car" }
     println!("{:?}", sister);   // Child { name: "sister", toy: "toy car" }
     println!("{:?}", Rc::strong_count(&toy));   // 3
     drop(brother);
     drop(sister);
     println!("{:?}", Rc::strong_count(&toy));   // 1
 }

可以通过 Rc::downgrade() 得到Weak<T>版本的智能指针类型,因为其共享的指针没有所有权,所以被称为弱引用。

特别的:Rc<T>只是用于单线程,不能用于多线程

5.5、UnsafeCell<T>,Cell<T>,RefCell<T>:内部可变性

如果一个类型可以通过共享引用&T来改变其内部数据,则该类型具有内部可变性。这个明显违反了Rust的借用规则:共享引用不能改变的。UnsafeCell<T>是Rust中唯一允许的可跳过这个规则的类型。即使UnsafeCell<T>是不可变的,依然可以安全的对其内部数据进行修改。对于UnsafeCell<T>,创建多个&mut UnsafeCell<T>是Undefined Behavior的。

UnsafeCell<T>基础上,你可以构建自己的内部可变性类型,某个字段包含为UnsafeCell<T>即可,如标准库中的RefCell<T>Cell<T>,以及std::sync::atomic中的很多类型。

更准确的定义上说,UnsafeCell<T>是智能指针,但是在其基础上构建出来的Cell<T>RefCell<T>等并不是智能指针,只能算是container。

对于引用和 Box<T>,借用规则的不可变性作用于编译时。对于RefCell<T>,不可变性作用于运行时,由此产生的差异是:引用如果违反规则得到编译错误,RefCell<T>违反规则会panic并退出

下面以Cell<T>RefCell<T>举例

 use std::cell::{Cell, RefCell};
 fn main() {
     let cell = Cell::new(1);
     cell.set(5);
     println!("{:?}", cell);
 ​
     let ref_cell = RefCell::new(String::new());
     ref_cell.borrow_mut().push_str("hello");
     println!("{:?}", ref_cell);
 }

5.6、Arc<T>:原子引用计数 - 线程安全

Arc是一个原子引用计数(Atomically Reference Counted)类型,其特点是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。因为线程安全带有性能惩罚,所以并没有默认为所有类型实现原子性,由使用者自由决定。Arc<T>拥有与Rc<T>相同的API,所有Rc版本的程序,只需要将Rc修改为Arc即可编译和运行

 use std::sync::{Mutex, Arc};
 use std::thread;
 ​
 fn main() {
     let counter = Arc::new(Mutex::new(0));
     let mut handles = vec![];
 ​
     for _ in 0..10 {
         let counter = Arc::clone(&counter);
         let handle = thread::spawn(move || {
             let mut num = counter.lock().unwrap();
 ​
             *num += 1;
         });
         handles.push(handle);
     }
 ​
     for handle in handles {
         handle.join().unwrap();
     }
 ​
     println!("Result: {}", *counter.lock().unwrap());
 }

5.7、Ref<T>,RefMut<T>

对于RefCell<T>类型而言,borrow()返回 Ref<T>,borrow_mut() 返回 RefMut<T>

6、编写你自己的智能指针

定义一个自定义的 CustomBox,并为其实现Deref trait和Drop trait

 use std::ops::Deref;
 struct CustomBox<T>(T);
 impl<T> CustomBox<T> {
     fn new(t: T) -> CustomBox<T> {
         CustomBox(t)
     }
 }
 impl<T> Deref for CustomBox<T> {
     type Target = T;
 ​
     fn deref(&self) -> &T {
         &self.0
     }
 }
 impl<T> Drop for CustomBox<T> {
     fn drop(&mut self) {
         println!("free resource here for CustomerBox!");
     }
 }

我们使用上述的自定义CustomBox来实现一个简单的DEMO

 fn demo(x: &i32) {
     println!("{:?}", x);
 }
 ​
 fn main() {
     let s = CustomBox::new(5);
     println!("{:?}", *s);
 ​
     let ss = CustomBox::new(CustomBox::new(CustomBox::new(6)));
     //之所以会工作是因为 Deref,重复解引用到需要的类型 &i32
     demo(&ss);
 }

7、参考资料

Reference:https://doc.rust-lang.org/stable/reference/introduction.html

The rust programming language 英文版:https://doc.rust-lang.org/book/title-page.html

The rust programming language 中文版:https://kaisery.github.io/trpl-zh-cn/title-page.html

《Rust编程之道》 by 张汉东

谢绝转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK