XiaoboTalk

Box 智能指针和隐式自动解引用

Rust 中常规的引用(reference 就是一种指针类型),指针就是一个指向具体值存储区域的箭头 →。
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
&x 会获取 x 的地址值(一个栈地址),这里使用 *y 解引用,来获取地址 y 中的值(即x中保存的值)。

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

上述代码也可以使用 Box 智能指针来实现:
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 也被称为智能指针。

构建自定义智能指针

struct MyBox<T>(T); // 元组类型的 struct,泛型参数 T impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } }
我们自己构建一个 MyBox,用于存储一个 T 值:
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 会报错:
$ 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。
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 背后做了什么

// ... 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 中保存任何值:
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 本身也是引用类型,我们在定义一个输出函数:
fn print_hello(name: &str) { println!("hello: {name}!"); }
print_hello 函数接收的是一个 &str 字符串切片类型,然后我们在 main 函数中调用它:
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 继续调用 Stringderef 方法,String 的 deref 默认返回一个 &str[…] ,最终执行 print_hello
如果没有隐式解引用,开发者就需要自己转换参数:
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() 来处理:
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() 方法:
c.drop(); // 编译报错
如果需要提前释放一些资源,例如,多线程的锁,那么需要调用系统提供的函数,std::mem::drop;
drop(c); // 释放 c 对象