Rust 中常规的引用(reference 就是一种指针类型),指针就是一个指向具体值存储区域的箭头 →。

rust
fn main() {
    let x = 5;
    let y = &x;

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

&x 会获取 x 的地址值(一个栈地址),这里使用 *y 解引用,来获取地址 y 中的值(即x中保存的值)。

使用 Box 来开辟堆上的地址空间

上述代码也可以使用 Box 智能指针来实现:

rust
fn main() {
    let x = 5;
    let y = Box::new(x);

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

区别在于,Box::new(x) 会将原来 x 中的值 5,拷贝一份,存储到堆上,这时 y 指向的是堆中的地址。

在 C 语言中,我们可以使用 malloc 等函数,开辟堆空间,在 Rust 中一般需要使用 Box,Box 也被称为智能指针。

构建自定义智能指针

rust
struct MyBox<T>(T); // 元组类型的 struct,泛型参数 T

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

我们自己构建一个 MyBox,用于存储一个 T 值:

rust
struct MyBox<T>(T); // 元组类型的 struct,泛型参数 T

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

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y); // 编译报错
}

上述代码,*y 会报错:

rust
$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error

原因是我们自己定义的 MyBox,默认无法使用 *y 运算来解引用,要想使用 * ,需要实现 Deref Trait。

rust
use std::ops::Deref;

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

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

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

deref 的要求,接收一个引用,并返回一个引用。因为解引用不需要转移所有权。这里 &self.0 会获取到 MyBox 中的 T 的具体值,这是元组类型的简写方式。此时便可以使用 *y 来解引用。

*y 背后做了什么

rust
// ...
fn main() {
    let x = 5;
    let y = MyBox::new(x);

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

事实上 *y 操作会被编译为: *(y.deref()) ,先调用 Deref 拿到类型想要解出的真实引用,然后再用 * 操作符取值。

函数/方法的多级隐式解引用

刚才自定义的 MyBox<T> 是一个泛型类型,意味着我们可以在 MyBox 中保存任何值:

rust
struct MyBox<T>(T); // 元组类型的 struct,泛型参数 T

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

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

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

fn main() {
    let s = String::from("rust");
    let y = MyBox::new(s);

    assert_eq!(5, x);
    assert_eq!(5, *y); // 编译报错
}

这里定义了一个 String,String 本身也是引用类型,我们在定义一个输出函数:

rust
fn print_hello(name: &str) {
    println!("hello: {name}!");
}

print_hello 函数接收的是一个 &str 字符串切片类型,然后我们在 main 函数中调用它:

rust
fn main() {
    let s = String::from("rust");
    let m = MyBox::new(s);
    hello(&m); // 成功打印:hello rust
}

这里,调用 print_hello 时传递的是 &m ,是一个指向 MyBox 的引用类型,但是最终的目标是需要一个 &str 切片,所以 Rust 类型系统会自动调用 MyBox 的 deref 去解引用,MyBox 的 deref 会获取到一个 &String ,依然不是 &str ,然后 rust 继续调用 String 的 deref 方法,String 的 deref 默认返回一个 &str[…] ,最终执行 print_hello 。

如果没有隐式解引用,开发者就需要自己转换参数:

rust
fn main() {
    let m = MyBox::new(String::from("rust"));
    hello(&(*(m.deref()))[..]);
}
  • m.deref() :获取到 MyBox 的 &String ,注意这里实际是个二级指针,指向一个 String 指针。
  • *(m.deref()) :解引用获取 String
  • &(*(m.deref()))[..] 将 String 转为 &str 切片。

隐式解引用,可以在不同类型间自动转换,包括,可变和不可变:

  • &T → &U 当 T 实现了: Deref<Target=U> ,上边的 MyBox 就是这样
  • &mut T → &mut U 当 T 实现了: DerefMut<Target=U>
  • &mut T → &U 当 T 实现了 Deref<Target=U>

一个可变引用是可以转为不可变的,因为可变引用要求同一时间,只有一个引用。所以可以安全的转为不可变引用。相反则无法转换。

除了函数参数上,. 语法调用的时候,也可以自动解引用。所以智能指针之所以智能,就是因为它可以自动解引用。

Drop Trait

Rust 中每个对象在超出其作用域的时候,编译器会自动插入并调用 drop 方法,用来处理一些资源释放的事情。如果自定义的对象需要在其被销毁前做一些资源释放(文件句柄释放),可以实现 drop() 来处理:

rust
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data {}", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

// 打印结果, drop 的调用和 create 顺序相反
// CustomSmartPointers created.
// Dropping CustomSmartPointer with data other stuff
// Dropping CustomSmartPointer with data my stuff

不能显式直接调用对象的 drop() 方法:

rust
c.drop(); // 编译报错

如果需要提前释放一些资源,例如,多线程的锁,那么需要调用系统提供的函数,std::mem::drop;

rust
drop(c); // 释放 c 对象