智能指针

本文大部分内容翻译自:The Rust Programming Language

1. Using Box to Point to Data on the Heap

2. Treating Smart Pointers Like Regular References with the Deref Trait

Implementing the Deref trait allows you to customize the behavior of the dereference operator * (not to be confused with the multiplication or glob operator).

2.1. Following the Pointer to the Value

例:

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}

The variable x holds an i32 value 5. We set y equal to a reference to x. We can assert that x is equal to 5. However, if we want to make an assertion about the value in y, we have to use *y to follow the reference to the value it’s pointing to (hence dereference) so the compiler can compare the actual value. Once we dereference y, we have access to the integer value y is pointing to that we can compare with 5.

2.2. Using Box Like a Reference

We can use a Box instead of a reference; the dereference operator used on the Box functions in the same way as the dereference operator:

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

The main difference between Listing 15-7 and Listing 15-6 is that here we set y to be an instance of a box pointing to a copied value of x rather than a reference pointing to the value of x.

2.3. Defining Our Own Smart Pointer

  • 为了能在自定义类型上使用*来解引用,需要为该类型实现Deref trait
  • Deref trait要求实现deref方法
  • deref方法以自身类型的引用(&self)为参数,返回类型内部数据的引用
    • deref方法返回数据的引用、而非数据本身的原因:由于所有权系统的存在,如果返回数据本身,就会将内部数据的所有权移出自定义类型(MyBox)。而大部分情况下,我们在解引用后并不需要获取MyBox内部数据的所有权。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::ops::Deref;

// 自定义类型MyBox
struct MyBox<T>(T);

// MyBox的关联函数new,用于返回MyBox的一个新实例
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}

// 为MyBox实现Deref trait,实现deref方法
impl<T> Deref for MyBox<T> {
// 定义一个Deref trait可用的关联类型(associated type)
type Target = T;

fn deref(&self) -> &Self::Target {
// `.0`访问元组结构体(tuple struct)的第1个元素。
// 所以这里,当我们用`*`来解引用时,deref函数被调用,
// 并返回MyBox<T>(T)这个元组结构体中第1个元素的引用
&self.0
}
}

使用MyBox

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

没有实现Deref trait的时候,编译器只懂得如何将&解引用。deref方法告诉编译器,如果想要解引用实现了Deref trait的类型,调用我(deref方法)就好了。deref方法会返回一个&引用,而对&引用进行解引用正是编译器懂得的。

上面assert_eq!(5, *y);中,Rust实际运行的是:

1
*(y.deref())

Rust将*替换成y.deref(),然后再对返回的结果&self.0进行它所熟知的、对&解引用,即*(&self.0)

2.4. 函数和方法的隐式 Deref 强制转换

Deref 强制转换(deref coercions)只能作用于实现了Deref trait的类型,它可以将这个实现了Deref trait的类型的引用,转换成另一个类型的引用。

例如,deref coercion可以将&String强制转换成&str,因为String类型实现了Deref trait,所以可以返回&str

当我们把一个与函数或者方法的参数类型不符的某种类型的引用作为参数传给它们时,会自动进行强制转换。这时会有一系列的deref方法被调用,把我们提供的类型转换成参数所需的类型。

例:

1
2
3
4
5
6
7
8
9
10
// hello的参数`name`的类型为`&str`
fn hello(name: &str) {
println!("Hello, {name}!");
}

// 用MyBox<String>类型的引用作为参数,调用hello
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
  • 上面代码使用&m作为参数调用hello函数,而&m实际上是MyBox<String>的引用,即&MyBox<String>
  • 由于我们为MyBox<T>实现了Deref trait,所以Rust可以通过调用MyBox<T>deref方法,将&MyBox<String>转换成&String
  • 标准库为String类型提供了Deref trait的实现,对String解引用会返回&str,因此Rust再次调用deref方法,将&String转换为&str,而&str正与hello函数的参数类型匹配。

如果Rust没有提供强制转换,我们就需要这样调用hello函数:

1
2
3
4
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
  • (*m)调用MyBox<String>类型的deref方法,将它解引用为String类型
    • *m <=> *(m.deref()) <=> *(&self.0) <=> self.0 => String类型
  • &[..]作用于String类型,就是从String类型取一段字符串切片(string slice),即&str,这个切片为整个字符串

当涉及到实现了Deref trait的类型的解引用时,Rust会分析这些类型,并按需自动调用任意多次Deref::deref方法,来获取与参数类型匹配的引用类型。

2.5. Deref 强制转换如何与可变性交互

我们可以使用Deref trait来重载(override)不可变引用的*运算符,类似地,也可以用DerefMut trait来重载可变引用的*运算符。

Rust进行强制转换的3种情况:

  • &T转换为&U,其中T: Deref<Target=U>
  • &mut T转换为&mut U,其中T: DerefMut<Target=U>
  • &mut T转换为&U,其中T: Deref<Target=U>

前两种情况除了可变性之外是相同的:

  • 第一种情况表明,如果有一个&T,其中T实现了DerefU类型,则可以直接得到&U
  • 第二种情况表明,对于可变引用也有着相同的行为

第三个情况有些微妙:

  • Rust可以将可变引用强转为不可变引用。但是反之是不可能的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果你有一个可变引用,那么它必须是数据的唯一引用(否则程序将无法编译)。
    • 将一个可变引用转换为不可变引用永远也不会打破借用规则,而将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,但借用规则无法保证这一点。

3. Running Code on Cleanup with the Drop Trait

4. Rc, the Reference Counted Smart Pointer

5. RefCell and the Interior Mutability Pattern

6. Reference Cycles Can Leak Memory

参考资料

[1] https://doc.rust-lang.org/stable/book/ch15-02-deref.html#following-the-pointer-to-the-value-with-the-dereference-operator