智能指针

  • 指针,常见的指针类型是引用,引用通过 & 符号表示。引用在 Rust 中被赋予了更深层次的含义,那就是:借用其它变量的值。引用本身很简单,除了指向某个值外并没有其它的功能
  • 智能指针,通过比引用更复杂的数据结构,包含比引用更多的信息,例如元数据,当前长度,最大可用长度等。String, Vec 都是智能指针

引用和智能指针的另一个不同在于前者仅仅是借用了数据,而后者往往可以拥有它们指向的数据,然后再为其它人提供服务。

智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 DerefDrop 特征:

  • Deref 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 *T
  • Drop 允许你指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作

智能指针在 Rust 中很常见:

  • Box<T>,可以将值分配到堆上
  • Rc<T>,引用计数类型,允许多所有权存在
  • Ref<T>RefMut<T>,允许将借用规则检查从编译期移动到运行期进行

Box

Box 的使用场景 由于 Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 Box 相比其它智能指针,功能较为单一,可以在以下场景中使用它:

  • 特意的将数据分配在堆上
  • 数据较大时,又不想在转移所有权时进行数据拷贝
  • 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时(递归类型)
  • 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型

Box::leak

Box 中还提供了一个非常有用的关联函数:Box::leak,它可以消费掉 Box 并且强制目标值从内存中泄漏. 使用场景:

  • 可以把一个 String 类型,变成一个 'static 生命周期的 &str
    fn main() {
     let s = gen_static_str();
     println!("{}", s);
    }
    fn gen_static_str() -> &'static str{
      let mut s = String::new();
      s.push_str("hello, world");
      Box::leak(s.into_boxed_str())
    }
  • 需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久,那么就可以使用 Box::leak; 例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然 Rc/Arc 也可以实现此功能,但是 Box::leak 是性能最高的。

Deref

一个类型为 T 的对象 foo,如果 T: Deref<Target=U>,那么,相关 foo 的引用 &foo 在应用的时候会自动转换为 &U。

函数和方法中的隐式 Deref 转换

对于函数和方法的传参,Rust 提供了一个极其有用的隐式转换:Deref 转换。若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换

fn main() {
    let s = String::from("hello world");
    display(&s)
}
fn display(s: &str) {
    println!("{}",s);
}
  • String 实现了 Deref 特征,可以在需要时自动被转换为 &str 类型
  • &s 是一个 &String 类型,当它被传给 display 函数时,自动通过 Deref 转换成了 &str
  • 必须使用 &s 的方式来触发 Deref(仅引用类型的实参才会触发自动解引用)
  • 表达式中不会执行隐式的解引用需要自己执行*

赋值操作需要手动解引用 方法调用会自动解引用

引用归一化

Rust 编译器实际上只能对 &v 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 &&&&v 类型的呢? 该如何对这两个进行解引用?

答案是:Rust 会在解引用时自动把智能指针和 &&&&v 做引用归一化操作,转换成 &v 形式,最终再对 &v 进行解引用:

  • 把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 &v
  • 把多重&,例如 &&&&&&&v,归一成 &v

Drop

Drop 的顺序

  • 变量级别,按照逆序的方式
  • 结构体内部,字段按照定义中的顺序依次 drop

Rc

引用计数(reference counting),当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者

Rc 是指向底层数据的不可变的引用.

use std::rc::Rc;
fn main() {
  let a = Rc::new(String::from("hello, world"));
  let b = Rc::clone(&a);

  assert_eq!(2, Rc::strong_count(&a)); // 引用计数
  assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}

Arc

Arc 是 Atomic Rc 的缩写,顾名思义:原子化的 Rc 智能指针。原子化是一种并发原语. Arc 和 Rc 拥有完全一样的 API

Cell

可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。 内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中.

Cell 和 RefCell 在功能上没有区别,区别在于 Cell 适用于 T 实现 Copy

use std::cell::Cell;
fn main() {
  let c = Cell::new("asdf");
  let one = c.get();
  c.set("qwer");
  let two = c.get();
  println!("{},{}", one, two);
}
  • "asdf" 是 &str 类型,它实现了 Copy 特征
  • c.get 用来取值,c.set 用来设置新值

RefCell

由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。

Rust 规则智能指针带来的额外规则
一个数据只有一个所有者Rc/Arc 让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用RefCell 实现编译期可变、不可变引用共存
违背规则导致编译错误违背规则导致运行时 panic

可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常

use std::cell::RefCell;
fn main() {
    let s = RefCell::new(String::from("hello, world"));
    let s1 = s.borrow();
    let s2 = s.borrow_mut();

    println!("{},{}", s1, s2);
}

RefCell 简单总结

  • 与 Cell 用于可 Copy 的值不同,RefCell 用于引用
  • RefCell 只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则
  • RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
  • 使用 RefCell 时,违背借用规则会导致运行期的 panic

选择 Cell 还是 RefCell

  • Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
  • Cell 不会 panic,而 RefCell 会

Weak

Weak 非常类似于 Rc,但是与 Rc 持有所有权不同,Weak 不持有所有权,它仅仅保存一份指向数据的弱引用:如果你想要访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回一个类型为 Option<Rc> 的值。

因为 Weak 引用不计入所有权,因此它无法阻止所引用的内存值被释放掉,而且 Weak 本身不对值的存在性做任何担保,引用的值还存在就返回 Some,不存在就返回 None。

Weak 与 Rc 对比

WeakRc
不计数引用计数
不拥有所有权拥有值的所有权
不阻止值被释放(drop)所有权计数归零,才能 drop
引用的值存在返回 Some,不存在返回 None引用的值必定存在
通过 upgrade 取到 Option<Rc>,然后再取值通过 Deref 自动解引用,取值无需任何操作

Weak 总结

Weak 通过 use std::rc::Weak 来引入,它具有以下特点:

可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收 可由 Rc 调用 downgrade 方法转换成 Weak Weak 可使用 upgrade 方法转换成 Option<Rc>,如果资源已经被释放,则 Option 的值是 None 常用于解决循环引用的问题

互斥锁 Mutex (mutual exclusion 的缩写)

Mutex让多个线程并发的访问同一个值变成了排队访问:同一时间,只允许一个线程A访问该值,其它线程需要等待A访问完成后才能继续 Mutex具有内部可变性

use std::sync::Mutex;

fn main() {
  let data = Mutex::new(0);
  let d1 = data.lock();
  let d2 = data.lock();
} // d1锁在此处释放

上面的代码会造成死锁

读写锁 RwLock

可以同时存在多个读操作,但同一时间只能有一个写操作

use std::sync::RwLock;
fn main() {
  let lock = RwLock::new(5);

  // 同一时间允许多个读
  {
    let r1 = lock.read().unwrap();
    let r2 = lock.read().unwrap();
    assert_eq!(*r1, 5);
    assert_eq!(*r2, 5);
  } // 读锁在此处被drop

  // 同一时间只允许一个写
  {
    let mut w = lock.write().unwrap();
    *w += 1;
    assert_eq!(*w, 6);

    // 以下代码会panic,因为读和写不允许同时存在
    // 写锁w直到该语句块结束才被释放,因此下面的读锁依然处于`w`的作用域中
    // let r1 = lock.read();
    // println!("{:?}",r1);
  }// 写锁在此处被drop
}

Atomic

Atomic的值具有内部可变性,你无需将其声明为mut