Rust 解引用

“解引用(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、Rc、Arc等。它们都支持“解引用”这个操作。
从某种意义上来说,它们都可以算做特种形式的“指针”,(像胖指针一样,是带有额外元数据的指针)。

  • &[T]是指针,指向一个数组切片;
  • &str是“指针”,指向一个字符串切片;

它们不仅包含了指向数据的指针,还携带了所指向的数据的长度信息,但它们对指向的数组/字符串切片没有所有权,不负责内存空间的分配和释放。

  • Box是“指针”,指向一个在堆上分配的对象;
  • Vec是“指针”,指向一组同类型的顺序排列的堆上分配的对象;
  • String是“指针”,指向的是一个堆上分配的字节数组,其中保存的内容是合法的 utf8 字符序列。

它们都对所指向的内容拥有所有权,管理着它们所指向的内存空间的分配和释放。

  • Rc和Arc也算是某种形式的“指针”,它们提供的是一种“共享”的所有权,当所有的引用计数指针都销毁之后,它们所指向的内存空间才会被释放。

自定义解引用操作符,可以让用户自行定义各种各样的“智能指针”,完成各种各样的任务。再配合上编译器的“自动”解引用机制,非常有用。下面我们讲解什么是“自动解引用”。

自动解引用

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>,位置(2)处,ps1的类型为Rc,位置(3)处,pps1的类型为String。

一般情况,在函数调用的时候,编译器会帮我们尝试自动解引用。但在其它情况下,编译器不会为我们自动插入自动解引用的代码。比如,以String和 &str类型为例:

fn main() {
  let s = String::new();
  match &s {
    "" => {}
    _ => {}
  }
}

这段代码编译会发生错误,错误信息为:

mismatched types:
 expected `&collections::string::String`,
    found `&'static str`

match 后面的变量类型是 &String,匹配分支的变量类型为 &str,这种情况下就需要我们手工完成类型转换了。为了将&String类型转换为&str类型,手工类型转换的话有哪些办法呢?

参考答案:

  1. match s.as_ref()。 这个方法是最通用最直观的办法。
  2. match s.borrow()。为了使用这个方法,我们必须引入Borrow trait。也就是需要加上代码use std::borrow::Borrow;。
  3. match s.deref()。 这个方法通过主动调用deref()方法,达到类型转换的目的。此时我们需要引入Deref trait方可以通过编译,即加上代码use std::ops::Deref;。
  4. match &*s。 我们可以通过*s运算符,也可以强制调用deref()方法,与上面的做法一样。
  5. match &s[..]。这个方案也是可以的,这里利用了String重载的Index操作。

总结

Rust中允许一部分运算符可以由用户自定义行为,类似其它语言中的“操作符重载”。其中解引用是一个非常重要的操作符,它允许重载。

而需要提醒大家注意的是,取引用操作符,如 & &mut 等,是不允许重载的。因此,取引用& 和 解引用* 并非对称互补关系。*&T的类型一定是T,而&*T 的类型未必就是 T。

更重要的是,读者需要理解,在某些情况下,编译器帮我们插入了自动 deref 的调用,简化代码。

你可能感兴趣的:(java,c++,jvm)