通过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支持解引用运算符:
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一样解引用:
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内部值的所有权。可以将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:
按照借用规则,如果存在一个可变引用,那么它就必须是唯一的引用(否则程序将无法通过编译)。将一个可变引用转换为不可变引用肯定不会破坏借用规则,但将一个不可变引用转换为可变引用则要求这个引用必须是唯一的,而借用规则无法保证这一点。