“解引用(Deref)”是“引用(Ref)”的反操作。比如说,我们有引用类型let p: &T;,那么可以用*符号执行解引用操作,let v: T = *p;。如果p的类型是&T, 那么*p的类型就是T。
解引用操作,可以被自定义。方法是,实现标准库中的std::ops::Deref和std::ops::DerefMut这两个 trait。
Deref的定义如下所示,DerefMut的唯一区别是返回的是&mut型引用,都是类似的,因此不过多做介绍了。
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
这个 trait 有一个关联类型 Target,代表解引用之后的目标类型。
比如说,标准库中,实现了String向str的解引用转换:
impl ops::Deref for String {
type Target = str;
#[inline]
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
请大家注意这里的类型,deref() 方法返回的类型是 &Target,而不是 Target。
如果说有变量s的类型为String,*s 的类型并不等于 s.deref() 的类型。
*s的类型实际上是 Target,即str。&*s的类型为&str。
而 s.deref() 的类型为 &Target,即 &str。它们的关系为:
s : String
&s : &String
Target : str
s.deref() : &str
*s : str
&*s : &str
标准库中,有许多我们常见的类型,实现了这个 Deref 操作符。比如 Vec
String、Box
从某种意义上来说,它们都可以算做特种形式的“指针”,(像胖指针一样,是带有额外元数据的指针)。
它们不仅包含了指向数据的指针,还携带了所指向的数据的长度信息,但它们对指向的数组/字符串切片没有所有权,不负责内存空间的分配和释放。
它们都对所指向的内容拥有所有权,管理着它们所指向的内存空间的分配和释放。
自定义解引用操作符,可以让用户自行定义各种各样的“智能指针”,完成各种各样的任务。再配合上编译器的“自动”解引用机制,非常有用。下面我们讲解什么是“自动解引用”。
Rust的设计理念一向是“显式比隐式好”。代码应该尽可能地将它的行为明显地表达出来,避免在看不见的地方“自动”帮我们做一些事情。
凡事都有例外。Rust中最容易被初学者误解的一个“隐式”行为就是这个“自动解引用”。什么是自动解引用呢,下面用一个示例来说明:
fn main() {
let s = "hello";
println!("length: {}", s.len());
println!("length: {}", (&s).len());
println!("length: {}", (&&&&&&&&&&&&&s).len());
}
编译发现,可以编译成功。我们知道,len这个方法的签名是:
fn len(&self) -> usize
它接受的参数是&str,因此我们可以用 UFCS 语法这么调用:
println!("length: {}", str::len(&s));
但是,我们如果使用&&&&&&&&&&str类型来调用成员方法,也是可以的。原因就是,Rust编译器帮我们做了隐式的 deref 调用,当它找不到这个成员方法的时候,它会自动尝试使用deref方法后再找该方法,一直循环下去。编译器在&&&str类型里面找不到len方法,就尝试将它deref,变成&&str类型,再寻找len方法,还是没找到,那么继续deref,变成&str,现在找到len方法了,于是就调用这个方法。
自动deref的规则是,如果类型T可以解引用为U,即T: Deref,则&T可以转为&U。
用Rc这个“智能指针”举例。Rc实现了Deref:
impl Deref for Rc {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
&self.inner().value
}
}
它的 Target 类型是它的泛型参数 T。这么设计有什么好处呢,我们看下面的用法:
use std::rc::Rc;
fn main() {
let s = Rc::new(String::from("hello"));
println!("{:?}", s.bytes());
}
我们创建了一个指向String类型的Rc指针,并调用了bytes()方法。这里是不是有点奇怪?
Rc类型本身并没有bytes()方法,所以编译器会尝试自动deref,试试s.deref().bytes()。String类型其实也没有bytes()方法,但是String可以继续deref,于是再试试s.deref().deref().bytes()。这次在str类型中,找到了bytes()方法,于是编译通过。
我们实际上通过Rc类型的变量,调用了str类型的方法,让这个智能指针像个透明的存在。这就是自动Deref的意义。
实际上以下写法在编译器看起来是一样的:
use std::rc::Rc;
use std::ops::Deref;
fn main() {
let s = Rc::new(String::from("hello"));
println!("length: {}", s.len());
println!("length: {}", s.deref().len());
println!("length: {}", s.deref().deref().len());
println!("length: {}", (*s).len());
println!("length: {}", (&*s).len());
println!("length: {}", (&**s).len());
}
注意:我们可以写let p = &*s;,它可以创建一个指向内部String的指针。这种写法不等于
let tmp = *s;
let x = &tmp;
因为这个tmp的存在,它表达的是move语义。也不等于
let x = &{*s};
这个大括号引入了新的 scope,同样也是move语义。
如果说,智能指针中的方法与它内部成员的方法冲突了怎么办呢?编译器会优先调用当前最匹配的类型,而不会执行自动 deref,这种情况下,我们就只能手动 deref 来表达我们的需求了。
比如说,Rc类型和String类型都有clone方法,但是它们执行的任务不同。Rc::clone()做的是把引用计数指针复制一份,把引用计数加1。String::clone()做的是把字符串复制一份。示例如下:
use std::rc::Rc;
use std::ops::Deref;
fn type_of(_: ()) { }
fn main() {
let s = Rc::new(Rc::new(String::from("hello")));
let s1 = s.clone(); // (1)
//type_of(s1);
let ps1 = (*s).clone(); // (2)
//type_of(ps1);
let pps1 = (**s).clone(); // (3)
//type_of(pps1);
}
在以上的代码中,位置(1)处,s1的类型为Rc
一般情况,在函数调用的时候,编译器会帮我们尝试自动解引用。但在其它情况下,编译器不会为我们自动插入自动解引用的代码。比如,以String和 &str类型为例:
fn main() {
let s = String::new();
match &s {
"" => {}
_ => {}
}
}
这段代码编译会发生错误,错误信息为:
mismatched types:
expected `&collections::string::String`,
found `&'static str`
match 后面的变量类型是 &String,匹配分支的变量类型为 &str,这种情况下就需要我们手工完成类型转换了。为了将&String类型转换为&str类型,手工类型转换的话有哪些办法呢?
参考答案:
Rust中允许一部分运算符可以由用户自定义行为,类似其它语言中的“操作符重载”。其中解引用是一个非常重要的操作符,它允许重载。
而需要提醒大家注意的是,取引用操作符,如 & &mut 等,是不允许重载的。因此,取引用& 和 解引用* 并非对称互补关系。*&T的类型一定是T,而&*T 的类型未必就是 T。
更重要的是,读者需要理解,在某些情况下,编译器帮我们插入了自动 deref 的调用,简化代码。