通过 Deref trait 为智能指针实现解引用
实现 Deref trait 可以自定义解引用运算符*的行为。通过实现 Deref,可以将智能指针视作常规引用来进行处理。
解引用的使用
与引用比较:
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, y); // error, 不能将引用与整数型对比
assert_eq!(5, *y); // 使用解引用运算符来跳转到引用指向的值:x
复制代码
利用引用改变原值:
let mut a = 1;
let b = &mut a;
*b = 2; // b是可变引用a,使用解引用运算符跳转到可变变量a,将a重新赋值为2
println!("{}", a); // 2
复制代码
在循环中使用:
let mut arr = [1;5];
for item in arr.iter_mut() {
*item *= 2 // 使用解引用运算符跳转到成员每一项指向的值,再乘2
}
println!("{:?}", arr); // [2, 2, 2, 2, 2]
复制代码
Box<T>支持解引用运算符:
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, y); // error, 不能将Box引用与整数型对比
assert_eq!(5, *y); // 使用解引用运算符来跳转到Box引用指向的值:x
复制代码
自定义智能指针
实现一个类似 Box 的自定义指针:
struct MyBox<T>(T); // MyBox是一个具有单个成员的元组结构体
impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
Self(value)
}
}
复制代码
像 Box<T>一样解引用:
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, y); // error, 不能将MyBox引用与整数型对比
assert_eq!(5, *y); // error,MyBox类型不能解引用
复制代码
通过实现 Deref trait 来使类型可解引用:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T; // 类型标注后边会有介绍,可先忽略
fn deref(&self) -> &T { // 返回成员的引用
&self.0
}
}
assert_eq!(5, *y); // 事实上*y会被编译成 *(y.deref())
复制代码
*号的行为
运算符包含两个行为:一个朴素的解引用 + deref()
首先通过 deref 获得一个引用,再使用朴素的解引用方式取到值,之所以 deref 返回的是一个引用,是因为如果返回一个值,那么这个值将被从 self 上移出,在大多数使用解引用运算符的场景下,我们并不希望获得 MyBox<T>内部值的所有权。可以将 deref 函数理解成:获取用于"解引用"的"引用类型数据"
函数和方法的隐式解引用转换
解引用转换(deref coercion)是 Rust 为函数和方法的参数提供的一种便捷特性。
加入类型 T 实现了 Deref trait,它能够将"T 的引用"转换为"T 经过 Deref 操作后生成的引用"。当我们将"某个类型的值引用"作为参数传递给函数或方法,但传入的类型与参数类型不一致时,解引用转换就会自动发生。编译器会插入一系列的 deref 方法调用来将我们提供的类型转换为参数所需的类型。
解引用 String:
fn hello(s: &str) {
println!("hello {}", s)
}
let s = String::from("world");
hello(s); // error,预期一个&str,却传入了String
hello(&s); // hello world,rust会自动将使用&String的deref,解引用后得到&str
复制代码
解引用 MyBox:
let m = MyBox(s);
hello(m); // error,预期一个&str,却传入了MyBox(String)
hello(&m); // hello world,对&MyBox(String)解引用得到&String,继续对&String解引用得到&str
复制代码
如果没有自动解引用功能只能手动解引用:
let a = &((*m)[..]);
// 首先将MyBox<String>进行解引用得到String,
// 然后,通过&和[..]来获取包含整个String的字符串切片以便匹配hello函数的签名
hello(a); // hello world
复制代码
解引用转换与可变性
使用 Deref trait 能够重载不可变引用的运算符。与之类似,使用 DerefMut trait 能够重载可变引用的运算符。
通过实现 DerefMut trait 来使可变类型可解引用:
use std::ops::DerefMut;
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
复制代码
实现 DerefMut trait 的前提是必须已经实现了 Deref trait
使用可变类型解引用:
let s = String::from("world");
let mut m = MyBox::new(s);
fn hi(s: &mut str) {
println!("hello {}", s)
}
hi(&mut m); // 自动使用deref_mut解可变引用
hi(&m); // error,不允许将不可变引用转为可变引用
复制代码
总结:
Rust 会在类型与 trait 满足下面 3 种情形时执行解引用转换:
当 T: Deref<Target=U>时,允许 &T 转换为 &U。
当 T: DerefMut<Target=U>时,允许 &mut T 转换为 &mut U。
当 T: Deref<Target=U>时,允许 &mut T 转换为 &U。
为什么 &mut T 可以转换为 &U,而 &T 不能转换为 &mut U:
按照借用规则,如果存在一个可变引用,那么它就必须是唯一的引用(否则程序将无法通过编译)。将一个可变引用转换为不可变引用肯定不会破坏借用规则,但将一个不可变引用转换为可变引用则要求这个引用必须是唯一的,而借用规则无法保证这一点。
评论