Rust的智能指针

79 阅读4分钟

什么指针?什么是智能指针?

指针,是保存内存地址的变量。Rust中最常见的指针是引用。引用没有值的所有权,它只能借用数据。 在栈上的值,当值离开作用域,这个值就会被清理掉,引用就会失效。

Rust是一个安全的语言,开发者一般不会使用裸指针,而是使用智能指针。 智能指针是一些数据结构,它们的表现像是指针,但是提供了附加的元信息和能力。智能指针有它指向的值的所有权。

接下来我们来看一些常见的智能指针。(我对Rust的智能指针也不是特别熟悉,所以本文只讲一部分智能指针,等我学会了更多,再来写新的文章介绍)

Box

这种智能指针是最简单的。 使用示例

fn main() {
    let a = Box::new(1);
    print!("{}", a); // 1
}

可以用它来实现递归类型,比如

enum List {
    Cons(i32, Box<List>),
    Nil,
}

这里举的例子是使用Box实现递归类型,其实任何指针都能实现递归类型的。但不用指针是无法实现递归类型的,原因是这样的递归类型的内存占用大小是无限的。

通过以下方式可以修改Box指向的结构体的字段,或者直接替换掉结构体的值。

#[derive(Debug)]
struct Point(i32, i32);

fn main() {
    let mut a = Box::new(Point(1, 2));
    a.as_mut().1 = 3;
    dbg!(&a);
    *a = Point(4, 5);
    dbg!(&a);
}

注意,Box没有实现Copy,也没实现Clone。把它赋值给其它的变量时,会发生移动。这看起来是合理的,这样可以保证它指向的值的所有者只有一个。当一个Box离开作用域时,它指向的值就会被释放。

但有时需要有多个指针指向同一个值,就得使用其它的智能指针了,比如Rc

Rc

它是有引用计数的智能指针。它可以任意clone。还可以获取引用计数。

let mut v = vec![];
let a = Rc::new(Point(1, 2));
v.push(a.clone());
dbg!(Rc::strong_count(&a));

当一个Rc指针离开作用域时,如果这个值没有引用了,就要把这个值清理调用。

这里有个问题:Rc不允许直接修改一个值。否则,就可能有多个可变引用,就可能违反借用规则。

为了解决这个问题,需要用使用RefCell。

RefCell

这种智能指针的关键的用途是获取它指向的值的可变引用。 “不能通过不可变引用修改数据”这个规则,使得一些本来安全的代码无法编译。 使用RefCell,可以绕开这个规则。但实际上,它在运行期间是遵守借用规则的。在运行时,如果同时存在多个可变引用,就会报错。所以,它仍然可以保证不让多个流程同时修改同一个值。 以下代码在运行时不会报错

let p = RefCell::new(Point(0, 0));
for _ in 0..10 {
    p.borrow_mut().0 += 1;
    println!("{:?}",&p);
}

以下代码在运行时会报错

let p = RefCell::new(Point(0, 0));
let a = p.borrow_mut();
let b = p.borrow_mut();

Rc结合RefCell

Rc和RefCell常常结合起来使用。

在实现数据结构时,常常会 遇到多个指针指向同一个值,而且需要这些指针都能修改这个值的情况。 这种需求,Rc和RefCell结合起来可以实现。

let mut p = Rc::new(RefCell::new(Point(0, 0)));
let mut p1 = p.clone();
p.borrow_mut().0 = 1;
p1.borrow_mut().1 = 2;
println!("{:?}", p); // RefCell { value: Point(1, 2) }

如何自定义智能指针?

开发者可以自己实现智能指针,只要实现DerefDrop这两个trait。

其中,Deref::deref用来定义这个指针解引用时得到的是哪个值的引用,也就是定义了这个指针指向的是哪个值。 Drop::drop用来定义这个指针离开定义域时执行什么操作,这是用来清理它指向的值的。

如何表示空值?

Rust是安全的,智能指针本身无法表示空值。要表示可空的智能指针类型,给智能指针套上Option<>即可

总结

以上就是Rust里常见的几种智能指针了。

在非并发场景下,Option<Rc<RefCell<T>>>这种使用方式,支持多个引用指向同一个值,而且都支持对值进行修改,也支持空值。它的灵活程度接近裸指针了,但它远比裸指针安全。 这种方式,应该足够在非并发场景下,实现任何数据结构了。

当然,只学会原理是不够的,还得多练习,才能熟练地使用它们。