Rust学习之智能指针

在 Rust 中,最常见的指针类型是引用,引用本身指向某个值,不会造成性能上的额外损耗。而智能指针比引用有更复杂的数据结构,包含比引用更多的信息,例如元数据,当前长度,最大可用长度等。引用和智能指针的另一个不同在于前者仅仅是借用了数据,而后者往往可以拥有它们指向的数据。智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:

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

动态字符串 String 和动态数组 Vec都是智能指针类型,还有Box<T>(可以将值分配到堆上)、Rc<T>(引用计数类型,允许多所有权存在)、Ref<T> 和 RefMut<T>(允许将借用规则检查从编译期移动到运行期进行)

Box<T>堆对象分配

Box<T> 允许将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。

栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说操作系统对栈内存的大小都有限制, 在 Rust 中,main 线程的栈大小是 8MB,普通线程是 2MB,在函数调用时会在其中创建一个临时栈空间,调用结束后 Rust 会让这个栈空间里的对象自动进入 Drop 流程,最后栈顶指针自动移动到上一个调用栈顶。与栈相反,堆上内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的。Rust 堆上的对象受所有权规则夫人限制,当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可)。

      对于堆栈上的性能,栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或 CPU 高速缓存。

Box的使用场景

Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗,功能较为单一,可以在以下场景中使用它:

1、使用Box<T>特意将数据分配在堆上

例如一个变量拥有一个数值 let a = 3,那变量 a 必然是存储在栈上的,那如果我们想要 a 的值存储在堆上就需要使用 Box<T>

fn main() {
    let a = Box::new(3);
    println!("a = {}", a); // a = 3
    // 下面一行代码将报错
    // let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
}

这样就创建一个智能指针指向了存储在堆上的 3,并且 a 持有了该指针。

  • println! 可以正常打印出 a 的值,是因为它隐式地调用了 Deref 对智能指针 a 进行了解引用
  • 最后一行代码 let b = a + 1 报错,是因为在表达式中,我们无法自动隐式地执行 Deref 解引用操作,你需要使用 * 操作符 let b = *a + 1,来显式的进行解引用
  • a 持有的智能指针将在作用域结束(main 函数结束)时,被释放掉,这是因为 Box<T> 实现了 Drop 特征

2、数据较大时,又不想在转移所有权时进行数据拷贝(避免栈上数据的拷贝)

栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。而堆上底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移:

fn main() {
    // 在栈上创建一个长度为1000的数组
    let arr = [0;1000];
    // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据
    let arr1 = arr;
    // arr 和 arr1 都拥有各自的栈上数组,因此不会报错
    println!("{:?}", arr.len());
    println!("{:?}", arr1.len());
    // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
    let arr = Box::new([0;1000]);
    // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
    // 所有权顺利转移给 arr1,arr 不再拥有所有权
    let arr1 = arr;
    println!("{:?}", arr1.len());
    // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
    // println!("{:?}", arr.len());
}

3、将动态大小类型变为 Sized 固定大小类型

Rust 需要在编译时知道类型占用多少空间,其中一种无法在编译时知道大小的类型是递归类型:

enum List {

Cons(i32, List),

Nil,

}

它的每个节点包含一个 i32 值,还包含了一个新的 List,Rust 认为该类型是一个 DST 类型,并在编译时报错。只需要使用Box<T>将 List 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)

enum List {

Cons(i32, Box<List>),

Nil,

}

4、特征对象

在 Rust 中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,而最常用的时特征对象

trait Draw {
    fn draw(&self);
}

struct Button {
    id: u32,
}
impl Draw for Button {
    fn draw(&self) {
        println!("这是屏幕上第{}号按钮", self.id)
    }
}

struct Select {
    id: u32,
}

impl Draw for Select {
    fn draw(&self) {
        println!("这个选择框贼难用{}", self.id)
    }
}

fn main() {
    let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];

    for e in elems {
        e.draw()
    }
}

代码中将不同类型的 Button 和 Select 包装成 Draw 特征的特征对象,放入一个数组中,Box<dyn Draw> 就是特征对象。特征也是 DST 类型,而特征对象在做的就是将 DST 类型转换为固定大小类型。

                   

Vec<i32> 的内存布局                        Vec<Box<i32>> 的内存布局

可以看出智能指针 vec2 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 Box 智能指针,最终 Box 智能指针又指向了存储在堆上的实际值。因此当我们从数组中取出某个元素时,取到的是对应的智能指针 Box,需要对该智能指针进行解引用,才能取出最终的值

fn main() {
    let arr = vec![Box::new(1), Box::new(2)];
    let (first, second) = (&arr[0], &arr[1]);
    let sum = **first + **second;
}

以上代码需注意两点:

  • 使用 & 借用数组中的元素,否则会报所有权错误
  • 表达式不能隐式的解引用,因此必须使用 ** 做两次解引用,第一次将 &Box<i32> 类型转成 Box<i32>,第二次将 Box<i32> 转成 i32

Box::leak 

Box 中还提供了一个非常有用的关联函数:Box::leak,它可以消费掉 Box 并且强制目标值从内存中泄漏。如果需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久,那么就可以使用 Box::leak。例如,可以把一个 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 我们不仅返回了一个 &str 字符串切片,它还是 'static 生命周期的!

Box 背后是调用 jemalloc 来做内存管理,所以堆上的空间无需我们的手动管理。一切皆对象 = 一切皆 Box

deref解引用 

deref 是 Rust 中最常见的隐式类型转换,而且它可以连续的实现如 Box<String> -> String -> &str 的隐式转换,只要链条上的类型实现了 Deref 特征。若一个类型实现了 Deref 特征,对它进行赋值或运算操作,需要手动解引用。若把它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换

fn main() {
    let s = MyBox::new(String::from("hello, world"));
    let s1: &str = &s;
    let s2: String = s.to_string();
    display(&s)
}

fn display(s: &str) {
    println!("{}",s);
}

对于 s1,我们通过两次 Deref 将 &str 类型的值赋给了它(赋值操作需要手动解引用);而对于 s2,我们在其上直接调用方法 to_string,是因为编译器对 MyBox 应用了 Deref进行了隐式转换方法调用会自动解引用)。String 实现了 Deref 特征,可以在需要时自动被转换为 &str 类型

函数或方法中,必须使用 &s 的方式来触发 Deref,因为仅引用类型的实参才会触发自动解引用。在display(&s)语句中,首先 MyBox 被 Deref 成 String 类型,结果并不能满足 display 函数参数的要求,编译器发现 String 还可以继续 Deref 成 &str,最终成功的匹配了函数参数。

fn main() {
    let x = Box::new(1);
    let sum = *x + 1;
}

智能指针 x 被 * 解引用为 i32 类型的值 1,然后再进行求和(运算操作需手动解引用)

 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,但不能把不可变引用隐式的转换成可变引用,规则如下:

  • 当 T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
  • 当 T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
  • 当 T: Deref<Target=U>,可以将 &mut T 转换成 &U
struct MyBox<T> {
    v: T,
}

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox { v: x }
    }
}

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.v
    }
}

use std::ops::DerefMut;

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.v
    }
}

fn main() {
    let mut s = MyBox::new(String::from("hello, "));
    display(&mut s)
}

fn display(s: &mut String) {
    s.push_str("world");
    println!("{}", s);
}
  • 要实现 DerefMut 必须要先实现 Deref 特征:pub trait DerefMut: Deref
  • T: DerefMut<Target=U> 解读:将 &mut T 类型通过 DerefMut 特征的方法转换为 &mut U 类型,对应上例中,就是将 &mut MyBox<String> 转换为 &mut String

Drop释放资源 

 对于 Drop 而言,主要有两个功能:

  • 回收内存资源
  • 执行一些收尾工作

 在绝大多数情况下,我们都无需手动去 drop 以回收内存资源,但有极少数情况,例如文件描述符、网络 socket 等,当这些值超出作用域不再使用时,自己来回收资源。在 Rust 中,我们可以指定在一个变量超出作用域时,执行一段特定的代码,最终编译器将帮我们自动插入这段收尾代码,这就是drop的收尾工作。

Rust 自动为几乎所有类型都实现了 Drop 特征,因此我们无需手动为自己定义的结构体实现drop特征,当然,如果要做一些自定义的收尾工作,也可以自行实现

struct Foo;

impl Drop for Foo {
    fn drop(&mut self) {
        println!("Dropping Foo!")
    }
}

fn main() {
    let _foo = Foo;
    println!("Running!");
}
输出:
Running!
Dropping Foo!

 Drop 特征中的 drop 方法借用了目标的可变引用,而不是拿走了所有权,而且结构体中每个字段都有自己的 Drop。

对于Drop的顺序,变量级别,按照逆序的方式,即后创建的变量会先被drop。结构体内部按照字段定义中的顺序依次 drop。

我们也可以提前去手动 drop,以提前释放资源。但是rust中不允许显式地调用析构函数,因此直接调用会编译出错,这里需要使用drop 函数,它拿走目标值的所有权,确保后续不再使用该值,而且调用drop函数无需引入任何模块信息,原因是该函数在std::prelude里。

#[derive(Debug)]
struct Foo;

impl Drop for Foo {
    fn drop(&mut self) {
        println!("Dropping Foo!")
    }
}

fn main() {
    let foo = Foo;
    drop(foo);  // 不能调用Drop特征的drop方法 foo.drop();
    // 以下代码会报错:借用了所有权被转移的值
    // println!("Running!:{:?}", foo);
}

我们不能为一个类型同时实现 Copy 和 Drop 特征,即实现了 Copy 的类型无法拥有析构函数。不然编译器会报错。

总之,我们无需担心变量及结构体的资源回收问题,因为rust会默认调用drop为我们做清理

Rc与Arc实现多所有权机制

在图数据结构和多线程场景中,可能会出现一个值需要有多个所有者的情况,为了解决此类问题,Rust 在所有权机制之外又通过引用计数的方式来简化相应的实现,它允许一个数据资源在同一时刻拥有多个所有者,这种实现机制就是 Rc 和 Arc,前者适用于单线程,后者适用于多线程。

Rc实现 

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

use std::rc::Rc;
fn main() {
        let a = Rc::new(String::from("test ref counting"));
        println!("count after creating a = {}", Rc::strong_count(&a));
        let b =  Rc::clone(&a);
        println!("count after creating b = {}", Rc::strong_count(&a));
        {
            let c =  Rc::clone(&a);
            println!("count after creating c = {}", Rc::strong_count(&c));
        }
        println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

以上代码我们使用 Rc::new 创建了一个新的 Rc<String> 智能指针并赋给变量 a,该指针指向底层的字符串数据。智能指针 Rc<T> 在创建时,还会将引用计数加 1,此时获取引用计数的关联函数 Rc::strong_count 返回的值将是 1

接着,我们又使用 Rc::clone 克隆了一份智能指针 Rc<String>,并将该智能指针的引用计数增加到 2。由于 a 和 b 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 2。这里的 clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此 a 和 b 是共享了底层的字符串 s,当然也可以使用 a.clone() 的方式来克隆,但推荐 Rc::clone 的方式。 

由于变量 c 在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,所以引用计数会减少 1,这是因为 Rc<T> 实现了 Drop 特征。当 ab 超出作用域后,引用计数会变成 0,最终智能指针和它指向的底层字符串都会被清理释放。

 Rc<T> 是指向底层数据的不可变的引用,因此无法通过它来修改数据, 但实际开发中我们往往需要对数据进行修改,此时就要配合其它数据类型,例如内部可变性的 RefCell<T> 类型以及互斥锁 Mutex<T>来一起使用。在多线程编程中,Arc 跟 Mutex 锁的组合使用非常常见,它们既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。

Arc实现

 Arc 是 Atomic Rc 的缩写,意思为:原子化的 Rc<T> 智能指针,它能保证我们的数据能够安全的在线程间共享,但会伴随着性能损耗。

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

fn main() {
    let s = Arc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Arc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}", s)
        });
    }
}

Rc 和 Arc 的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。还有就是:Arc 和 Rc 并没有定义在同一个模块,前者通过 use std::sync::Arc 来引入,后者通过 use std::rc::Rc。 

Rc与Arc在源码上有如下差异:

// Rc源码片段

impl<T: ?Sized> !marker::Send for Rc<T> {}

impl<T: ?Sized> !marker::Sync for Rc<T> {}

// Arc源码片段

unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}

unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

!代表移除特征的相应实现,上面代码中Rc<T>SendSync特征被特地移除了实现,而Arc<T>则相反,实现了Sync + SendSendSync是在线程间安全使用一个值的关键。 

Send与Sync

Send与Sync都只是标记特征(marker trait),实现Send的类型可以在线程间安全的传递其所有权,实现Sync的类型可以在线程间安全的共享(通过引用)。一个类型要在线程间安全的共享的前提是,指向它的引用必须能在线程间传递。即有命题:若类型 T 的引用&TSend,则TSync

在 Rust 中,几乎所有类型都默认实现了SendSync ,对于复合类型(例如结构体), 只要它内部的所有成员都实现了Send或者Sync,那么它就自动实现了SendSync。还有如下几类比较常见的类型没有实现Send或者Sync:

  • 裸指针两者都没实现,因为它本身就没有任何安全保证
  • UnsafeCell不是Sync,因此CellRefCell也不是
  • Rc两者都没实现(因为内部的引用计数器不是线程安全的)
  • Mutex<T>中的T并没有Sync特征约束。但RwLock两者都实现了的 

例如为裸指针实现Send与Sync特征的例子:

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

#[derive(Debug)]
struct MyBox(*const u8);
unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}

fn main() {
    let b = &MyBox(5 as *const u8);
    let v = Arc::new(Mutex::new(b));
    let t = thread::spawn(move || {
        let _v1 =  v.lock().unwrap();
    });

    t.join().unwrap();
}

 *const u8是一个裸指针,如果不具有Send特征,它将无法被安全地传递到新的线程中去,然而它确实并没有实现Send特征,而且又不能手动为其实现,但可以将其包装成复合类型:MyBox(*const u8),并为该结构体实现Send特征来解决该问题。SendSyncunsafe特征,实现时需要用unsafe代码块包裹。在多线程中我们不能直接去借用其它线程的变量,因为编译器无法确定主线程main和子线程谁的生命周期更长,借用何时被释放,因此这里需要配合Arc去使用。

上面代码将智能指针v的所有权转移给新线程,同时v包含了一个引用类型b,当在新的线程中试图获取内部的引用时,如果MyBox不具有Sync特征的话,代码将会报错。因为我们访问的引用实际上还是对主线程中的数据的借用,转移进来的仅仅是外层的智能指针引用。因此我们要为MyBox实现Sync特征: unsafe impl Sync for MyBox {}

Cell与Refcell

 Rust 提供了 Cell 和 RefCell 用于内部可变性,可以在拥有不可变引用的同时修改目标数据。  

Cell 

Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 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 特征。如果尝试在 Cell 中存放String,如:let c = Cell::new(String::from("asdf"));编译器会报错。c.get 用来取值,c.set 用来设置新值。Cell 没有额外的性能损耗。

Refcell

RefCell 用于引用,它只是将借用规则从编译期推迟到程序运行期,并不能帮绕过这个规则,当违背借用规则会导致运行期的 panic。RefCell 由于是非线程安全的,因此无需保证原子性。RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时。

RefCell 其实是有一点运行期开销的,原因是它包含了一个字节大小的“借用状态”指示器,该指示器在每次运行时借用时都会被修改,进而产生一点开销。

在某些场景中,需要一个值可以在其方法内部被修改,同时对于其它代码不可变。例如:

// 定义在外部库中的特征
pub trait Messenger {
    fn send(&self, msg: String);
}

// --------------------------
// 我们的代码中的数据结构和实现
struct MsgQueue {
    msg_cache: Vec<String>,
}

impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.push(msg)
    }
}

如上所示,外部库中定义了一个消息发送器特征 Messenger,send方法中使用了 &self 的不可变借用。而在自己的代码中使用该特征实现了一个异步消息队列,在 send 方法中,需要将消息先行插入到本地缓存 msg_cache 中。而 send 方法的签名是 &self,因此上述代码会报错。而此时又不能将方法签名中的 &self 修改为 &mut self,因为实现的特征是定义在外部库中的,此时就需要Refcell,通过包裹一层 RefCell,成功的让 &self 中的 msg_cache 成为一个可变值,然后实现对其的修改。

use std::cell::RefCell;
pub trait Messenger {
    fn send(&self, msg: String);
}

pub struct MsgQueue {
    msg_cache: RefCell<Vec<String>>,
}

impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.borrow_mut().push(msg)
    }
}

fn main() {
    let mq = MsgQueue {
        msg_cache: RefCell::new(Vec::new()),
    };
    mq.send("hello, world".to_string());
}

在 Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性:

use std::cell::RefCell;
use std::rc::Rc;
fn main() {
    let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));

    let s1 = s.clone();
    let s2 = s.clone();
    // let mut s2 = s.borrow_mut();
    s2.borrow_mut().push_str(", oh yeah!");

    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

运行结果:
RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }
RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }
RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }

我们使用 RefCell<String> 包裹一个字符串,同时通过 Rc 创建了它的三个所有者:ss1s2,并且通过其中一个所有者 s2 对字符串内容进行了修改。由于 Rc 的所有者们共享同一个底层的数据,修改会导致全部所有者持有的数据都发生了变化。

在 Rust 1.37 版本中新增了两个非常实用的方法:

  • Cell::from_mut,该方法将 &mut T 转为 &Cell<T>
  • Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell<T>]

使用这两个方法可以用来解决一些常见的借用冲突问题 

use std::cell::Cell;

fn is_even(i: i32) -> bool {
    i % 2 == 0
}

fn retain_even(nums: &mut Vec<i32>) {
    let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..])
        .as_slice_of_cells();

    let mut i = 0;
    for num in slice.iter().filter(|num| is_even(num.get())) {
        slice[i].set(num.get());
        i += 1;
    }

    nums.truncate(i);
}

总结以下,我们将所有权、借用规则与这些智能指针做一个对比:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值