Rust笔记 - 智能指针

239 阅读4分钟

Rust 作为low level语言,拥有指针是必不可少的。指针能直接操作内存,在内存间建立复杂的关联。Rust 支持像C++一样的智能指针和像C一样的裸指针。下面围绕智能指针展开。

std::boxed::Box

Box就像是一个盒子,保存对象在堆上分配的内存地址,对外表现为指针的形式。Rust中不是有所有权转移机制引用了吗?为什么还需要一个类似的东西呢?试想一下,一个结构体内部需要保存自身类型的成员时,使用所有权转移机制将面临无法确定结构体大小的问题;使用引用将面临生存期的问题。此时引入一个Box<T>类型的智能指针能将结构体打包到其中,重要的是Box<T>是能确定大小的。下面看一个例子:

#[derive(Debug)]
struct List {
    data: i32,
    next: Box<Option<List>>,
}

fn main() {
    let l = Box::new(Some(List {
        data: 1,
        next: Box::new(Some(List {
            data: 2,
            next: Box::new(Some(List {
                data: 3,
                next: Box::new(None),
            })),
        })),
    }));

    let mut cl = &l;
    while let Some(ref e) = **cl {
        println!("{}", e.data); // 依次输出:1 2 3
        cl = &e.next;
    }
}

上面的例子使用struct来实现了一个list(更好的做法是使用enum来实现),简单演示了Box<T>的用法。

std::rc::Rc

rcReference Counted的简写,意思是:引用计数。rc有那些特点:

  • 引用是不可变的。因为需要遵循多读者,单写者的规则。
  • 不允许线程间共享变量。因为改变引用计数不是线程安全的(没有实现Send trait)。

下面简单看一个例子:

use std::rc;

fn main() {
    let s = rc::Rc::new("hello".to_owned());
    let s1 = s.clone();
    let s2 = s.clone();
    drop(s);

    println!("{}", s1); // 输出:hello
    println!("{}", s2); // 输出:hello
}

上面的例子如果使用let s1 = &s; 进行共享变量,在drop(s)后,变量s1会变成未初始化。而使用 rc 通过增加变量s的引用计数,直到变量s的引用计数变为零时,变量才会真正被销毁。所以上面的例子能够正常编译运行。


如果想要修改 rc 中变量的内容,有什么方法呢?Rust 为我们提供了CellRefCell,可以修改共享 rc 变量的内容。下面看一个例子:

use std::rc;
use std::cell;

fn main() {
    let s = rc::Rc::new(cell::RefCell::new("hello".to_owned()));
    let s1 = s.clone();
    let s2 = s.clone();
    drop(s);

    s1.borrow_mut().push_str(" world");
    println!("{}", s1.borrow()); // 输出:hello world
    println!("{}", s2.borrow()); // 输出:hello world
    
    let v = rc::Rc::new(cell::Cell::new(1));
    let v1 = v.clone();
    drop(v);
    v1.set(2);
    println!("{}", v1.get()); // 输出: 2
}

上面例子需要注意的是:

  • Cell<T>RefCell<T> 变量不能线程共享。
  • 使用cell 并没有违反 多读者,单写者的规则。因为在cell 仅能用于单线程;获取具体的变量时,都需要通过类似指针的方式获取/修改变量,不会存在悬空的情况。

如果存在循环引用的情况,rc 变量的引用计数就不会变为0,变量也的不到销毁,造成内存泄露。那有什么方法解决这个问题呢?先看一个例子:

use std::cell;
use std::rc;

struct Parent {
    data: i32,
    c: rc::Rc<Child>,
}

struct Child {
    data: i32,
    p: cell::RefCell<Vec<rc::Weak<Parent>>>,
}

fn main() {
    let c = rc::Rc::new(Child {
        data: 2,
        p: cell::RefCell::new(vec![]),
    });

    let p = rc::Rc::new(Parent {
        data: 1,
        c: c.clone(),
    });

    {
        let mut cp = c.p.borrow_mut();
        cp.push(rc::Rc::downgrade(&p));
    }

    println!("parent->child: {}", p.c.data);
    println!("child->parent: {}", c.p.borrow()[0].upgrade().unwrap().data);
}

输出:

parent->child: 2
child->parent: 1

上面的例子通过弱引用计数来解决循环引用的问题。那什么是弱引用呢?rc引用存在强引用弱引用强引用是一个变量存活的关键,计数不为0,则变量不会被销毁,通过clone()出来的变量会增加该引用计数,强引用也是我们接触最多的引用计数。而弱引用则相对于强引用弱引用不控制变量的生存期。即,一个变量的强引用计数到达零,而弱引用计数不为零,变量也会销毁。可以通过弱引用来获取来获取变量的引用,达到访问变量的效果。

std::sync::Arc

ArcAtomically Reference Counted的简写,是原子引用计数的意思。原子说明该变量能够在线程间共享,不会存在rc 引用计数竞争的问题。那Arc有什么特点呢?

  • 线程安全

  • 引用计数的变量默认是不可变的,可以通过MutexRwLockAtomic类型包装成可变

  • 同样存在循环引用的问题,可以通过Weak引用解决。

下面看一个例子:

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

fn main() {
    let s = Arc::new("hello".to_string());
    let sc = s.clone();

    let hdl = thread::spawn(move || {
        println!("thread print: {}", sc);
    });

    println!("main thread print: {}", s);
    hdl.join().unwrap();
}

上面是简单的示例,使用方法和rc基本一致。