5分钟速读之Rust权威指南(二十五)Deref

通过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:

按照借用规则,如果存在一个可变引用,那么它就必须是唯一的引用(否则程序将无法通过编译)。将一个可变引用转换为不可变引用肯定不会破坏借用规则,但将一个不可变引用转换为可变引用则要求这个引用必须是唯一的,而借用规则无法保证这一点。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享