(转F001) Rust : 解引用,自动解引用

来源: https://zhuanlan.zhihu.com/p/21615411

“解引用(Deref)”是“引用(Ref)”的反操作。

比如说,我们有引用类型let p: &T;,那么可以用*符号执行解引用操作,let v: T = * p;
如果 p的类型是 &T, 那么 * p的类型就是T。

补充:解引用是针对有引用类型而言的,比如,下面值类型的解引用会报错!【个人补充】

//报错1
    let b = 5_i32;
    let c = *b;
//报错2、3:对Var、 Shape对象进行解引用会报错
    struct Var { value:i32};
    impl std::ops::Deref for Var {
         type Target = i32;
         fn deref(&self) -> &i32 {
              println!("->deref");
              &self.value
         }
     }

  struct Shape {
       val: Var,
  }
  impl std::ops::Deref for Shape {
        type Target = Var;
        fn deref(&self) -> &Var {
             println!("deref shape is success!");
             &self.val
        }
   }

自定义解引用

解引用操作,可以被自定义。方法是,实现标准库中的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,代表解引用之后的目标类型。

比如,我们可以随便写一个自定义的Deref 【补充】

struct  Var { value : i32 }
impl std::ops::Deref for Var {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.value}
}

标准库中,实现了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 -> Target                 // 解引用
s.deref()-> &Target     // deref() 
&*s ->s.deref()            

需要说明的是,*s不等于s.deref().这是两回事!

它们的关系为:

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。

Rust在以下三种情况下执行Deref强制:
当T:Deref <Target = U>其中T和U是不可变引用时,则&T转换为&U类型。
当T:DerefMut <Target = U>,其中T和U是可变引用时,则&mut T被转换为&mut U。
当T:Deref <Target = U>,其中T是可变引用而U是不可变引用,则&mut T被转换为&U。

规则: 在“隐式自动deref”的场景,当&T类型不匹配,编译器就会继续尝试&*T来进行匹配。
比如 &Vec -> &*Vec ->&[T]
比如 &String -> &*String ->&str

自动解引用的用处

用Rc这个“智能指针”举例。Rc实现了Deref:

impl<T: ?Sized> Deref for Rc<T> {
    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<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类型,手工类型转换的话有哪些办法呢?

参考答案:

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

总结

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

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

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

​本文同步发布于微信公众号:Rust编程,欢迎关注。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值