Rust智能指针

官方文档

在《The Book》第15章中如此描述:

A pointer is a general concept for a variable that contains an address in memory

这句话表明:指针是一个指向地址的变量

在Rust中我们最常用到的引用就是指针。引用不会带来额外损耗,只会借用数据。

Smart pointers, on the other hand, are data structures that act like a pointer but also have additional metadata and capabilities

与引用不同的是,智能指针表现为具有额外元数据,能力的指针。

Rust, with its concept of ownership and borrowing, has an additional difference between references and smart pointers: while references only borrow data, in many cases, smart pointers own the data they point to.

引用和智能指针一个重要区别是,引用借用数据,智能指针大多情况下拥有数据。

Smart pointers are usually implemented using structs. Unlike an ordinary struct, smart pointers implement the Deref and Drop traits。

从官方定义来看,智能指针重要特征是实现了DerefDrop

我们先分析下Deref。

Deref解引用

解引用deref(*)是引用(&)的反操作。如果我们对一个引用 使用*操作符,将读到地址指向的内存。相关trait:

1
2
3
4
5
6
7
8
9
10
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_unstable(feature = "const_deref", issue = "88955")]
impl<T: ?Sized> const Deref for &T {
type Target = T;

#[rustc_diagnostic_item = "noop_method_deref"]
fn deref(&self) -> &T {
*self
}
}

需要注意的是:上面实现Deref的是&T ,而 &self 脱糖后为 self: &Self 所以self实际的含义是&&T 。因此返回:*self -> &T

deref这个操作对应的trait如下:

1
2
3
4
5
pub trait Deref {
type Target: ?Sized;

fn deref(&self) -> &Self::Target;
}

将返回Target的引用,而不是Target

那么*操作符和deref有什么区别吗

  • * 操作符 => *Deref::deref(&g)
  • deref => Deref::deref(&g)

*操作符在deref调用后转为Target的引用,再调用其调用* 操作符。

我们测试一个自定义的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::ops::Deref;
struct MyPointer<T> {
t: T,
}
impl<T> Deref for MyPointer<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.t
}
}

#[test]
fn f() {
let g = MyPointer{
t: String::from("hello"),
};
let h = g.deref();
assert_eq!(h, "hello");
}

DerefMut的唯一区别是返回的是&mut型引用,不过多做介绍了。

自动解引用

比如如下的行为:

1
2
let mut i = Box::new(9);
dbg!(i.add(3));

Box似乎没有实现过add方法,这里实际是调用的i32的add。所谓自动解引用,实际上是某种连续隐式转换,直到找到想要的方法为止。这种隐式转换可以避免写出非常hack的代码,但是却会让代码可读性降低。
此外,有时我们确实会不需要这种解引用。比如,我们想对Arc<T> 调用clone() ,最终却clone了T。此时我们可以检查标准库这类智能指针是否实现了对应的function,Arc::clone()可以解决我们的问题。

Drop

Drop trait 类似结构体生命周期结束的回调。

1
2
3
4
5
6
7
8
9
10
11
#[test]
fn f() {
let a = A;
}

struct A;
impl Drop for A {
fn drop(&mut self) {
println!("droped");
}
}

那么问题来了

1. 如果我不实现这个Trait,难道这个struct就不回收了吗?

这里是一个例证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[test]
fn f() {
let a = A { c: C, b: B };
}

struct A {
c: C,
b: B,
}

struct B;

struct C;

impl Drop for B {
fn drop(&mut self) {
println!("drop B");
}
}

impl Drop for C {
fn drop(&mut self) {
println!("drop C");
}
}

上面代码可以打印出

1
2
drop C
drop B

2. 如果我实现了这个Trait,但是在里面什么都不做,那他还能顺利回收吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32


#[test]
fn f() {
let a = A { c: C, b: B };
}

struct A {
c: C,
b: B,
}
impl Drop for A {
fn drop(&mut self) {
println!("Drop A");
}
}
struct B;

struct C;

impl Drop for B {
fn drop(&mut self) {
println!("drop B");
}
}

impl Drop for C {
fn drop(&mut self) {
println!("drop C");
}
}

上面的代码可以会打印出:

1
2
3
Drop A
drop C
drop B

我们的结论是:Drop Trait实现与否都不会影响字段回收,先执行当前struct的Drop(如果实现了的话),然后按照:

  • 变量级别,按照逆序的方式
  • 结构体内部,按照顺序的方式
    的顺序,调用字段的drop,并递归执行字段的回收。

一个常见的坑是:Copy Trait和Drop Trait冲突

1
2
3
4
5
6
7
8
9
10
#[derive(Debug,Clone, Copy)]
struct A {
a: i32,
b: i32,
}
impl Drop for A {
fn drop(&mut self) {
println!("droped {} {}", self.a,self.b);
}
}
1
2
3
4
5
6
error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
--> src/smart_pointer.rs:41:23
|
41 | #[derive(Debug,Clone, Copy)]
| ^^^^ `Copy` not allowed on types with destructors
|

Explicitly implementing both Drop and Copy trait on a type is currently disallowed. This feature can make some sense in theory, but the current implementation is incorrect and can lead to memory unsafety (see issue #20126), so it has been disabled for now.

简而言之,Copy Drop共存理论上是能共存的,但是当前实现不正确,会导致内存不安全,已经关闭了。

Box

将数据保存在堆上,最常见的智能指针。

1. 使用场景

1. 避免栈上大量数据的拷贝

1. 有时我们需要申请大块内存时如果直接分配在栈上,如果发生所有权转移将会造成数据深拷贝。
2. 如果分配在堆上,则只是Box智能指针的转移
1
2
3
let arr = Box::new([0;1000]);
let arr1 = arr;
println!("{:?}", arr1.len());

2. 可以将动态大小的数据封装为固定大小

当我们执行let 语句时就意味着在栈上分配内存,而对于Rust要求栈分配时内存必须是编译器可知的,因此对于切片,trait object类对象必须经过Box封装后才能在栈上分配。

2. 堆上的内存什么时候释放呢?

1
2
3
4
5
6
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
fn drop(&mut self) {
// FIXME: Do nothing, drop is currently performed by compiler.
}
}

看起来Box指针的释放由编译器优化,并没有将实际执行逻辑写在alloc库中。但是查阅文档来看,这里实际上就是执行逻辑就是调用Drop并释放内存。

3. Box中的数据如何取出?

如果我们的目的是将box中的数据move出来。使用* 解引用即可。

1
2
let i = Box::new(5);
assert_eq!(*i, 5);

4. 裸指针和Box

裸指针和Box可以互转

1
2
3
let r = Box::into_raw(Box::new(String::from("abc")));

let n = unsafe { Box::from_raw(r) };

获取裸指针safe,将裸指针转为box则是unsafe。

Rc和Arc

引用计数智能指针。当我们需要将某个对象供程序的多个部分同时使用时,使用此类指针可以让我们避免内存泄露。

简而言之,当我们clone Rc/Arc时,我们并不会clone其内部数据,而在Rc指针的元信息上包含引用计数,clone只是增加了引用计数1;

同理Drop也不会释放其包含的数据,只是将其计数减1。当其引用计数为0时,才会释放其数据,释放内存。

Rc

相对于Arc,Rc没有实现Send,因此无法将Rc move到另一个线程。

弱引用

为了避免引用计数可能导致的循环引用问题,weak指针不能像强引用一样阻止Rc包含的数据Drop。

1
2
3
let g = Rc::new(S { i: 0 });
assert_eq!(Rc::strong_count(&g), 1);
assert_eq!(Rc::weak_count(&g),0);

使用downgrade 可以获取弱引用Weak

1
2
let h = Rc::downgrade(&g);
assert_eq!(h.upgrade().unwrap().i,0);

weak想要重新变回Rc则需调用upgrade,由于weak不能确认此时Rc有足够的强引用,数据可能已Drop,此时从弱引用变回强引用则是Option。