• Rust 解引用


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

    自定义解引用

    解引用操作,可以被自定义。方法是,实现标准库中的std::ops::Deref和std::ops::DerefMut这两个 trait。


    Deref的定义如下所示,DerefMut的唯一区别是返回的是&mut型引用,都是类似的,因此不过多做介绍了。

    1. pub trait Deref {
    2. type Target: ?Sized;
    3. fn deref(&self) -> &Self::Target;
    4. }
    5. pub trait DerefMut: Deref {
    6. fn deref_mut(&mut self) -> &mut Self::Target;
    7. }

    这个 trait 有一个关联类型 Target,代表解引用之后的目标类型。

    比如说,标准库中,实现了String向str的解引用转换:

    1. impl ops::Deref for String {
    2. type Target = str;
    3. #[inline]
    4. fn deref(&self) -> &str {
    5. unsafe { str::from_utf8_unchecked(&self.vec) }
    6. }
    7. }

    请大家注意这里的类型,deref() 方法返回的类型是 &Target,而不是 Target。
    如果说有变量s的类型为String,*s 的类型并不等于 s.deref() 的类型。
    *s的类型实际上是 Target,即str。&*s的类型为&str。
    而 s.deref() 的类型为 &Target,即 &str。它们的关系为:

    1. s : String
    2. &s : &String
    3. Target : str
    4. s.deref() : &str
    5. *s : str
    6. &*s : &str

    标准库中,有许多我们常见的类型,实现了这个 Deref 操作符。比如 Vec
    String、Box、Rc、Arc等。它们都支持“解引用”这个操作。
    从某种意义上来说,它们都可以算做特种形式的“指针”,(像胖指针一样,是带有额外元数据的指针)。

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

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

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

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

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

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

    自动解引用

    Rust的设计理念一向是“显式比隐式好”。代码应该尽可能地将它的行为明显地表达出来,避免在看不见的地方“自动”帮我们做一些事情。

    凡事都有例外。Rust中最容易被初学者误解的一个“隐式”行为就是这个“自动解引用”。什么是自动解引用呢,下面用一个示例来说明:

    1. fn main() {
    2. let s = "hello";
    3. println!("length: {}", s.len());
    4. println!("length: {}", (&s).len());
    5. println!("length: {}", (&&&&&&&&&&&&&s).len());
    6. }

    编译发现,可以编译成功。我们知道,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:

    1. implSized> Deref for Rc {
    2. type Target = T;
    3. #[inline(always)]
    4. fn deref(&self) -> &T {
    5. &self.inner().value
    6. }
    7. }

    它的 Target 类型是它的泛型参数 T。这么设计有什么好处呢,我们看下面的用法:

    1. use std::rc::Rc;
    2. fn main() {
    3. let s = Rc::new(String::from("hello"));
    4. println!("{:?}", s.bytes());
    5. }

    我们创建了一个指向String类型的Rc指针,并调用了bytes()方法。这里是不是有点奇怪?


    Rc类型本身并没有bytes()方法,所以编译器会尝试自动deref,试试s.deref().bytes()。String类型其实也没有bytes()方法,但是String可以继续deref,于是再试试s.deref().deref().bytes()。这次在str类型中,找到了bytes()方法,于是编译通过。

    我们实际上通过Rc类型的变量,调用了str类型的方法,让这个智能指针像个透明的存在。这就是自动Deref的意义。

    实际上以下写法在编译器看起来是一样的:

    1. use std::rc::Rc;
    2. use std::ops::Deref;
    3. fn main() {
    4. let s = Rc::new(String::from("hello"));
    5. println!("length: {}", s.len());
    6. println!("length: {}", s.deref().len());
    7. println!("length: {}", s.deref().deref().len());
    8. println!("length: {}", (*s).len());
    9. println!("length: {}", (&*s).len());
    10. println!("length: {}", (&**s).len());
    11. }

    注意:我们可以写let p = &*s;,它可以创建一个指向内部String的指针。这种写法不等于

    1. let tmp = *s;
    2. let x = &tmp;

    因为这个tmp的存在,它表达的是move语义。也不等于

    let x = &{*s};
    

    这个大括号引入了新的 scope,同样也是move语义。

    有时候需要手动处理

    如果说,智能指针中的方法与它内部成员的方法冲突了怎么办呢?编译器会优先调用当前最匹配的类型,而不会执行自动 deref,这种情况下,我们就只能手动 deref 来表达我们的需求了。

    比如说,Rc类型和String类型都有clone方法,但是它们执行的任务不同。Rc::clone()做的是把引用计数指针复制一份,把引用计数加1。String::clone()做的是把字符串复制一份。示例如下:

    1. use std::rc::Rc;
    2. use std::ops::Deref;
    3. fn type_of(_: ()) { }
    4. fn main() {
    5. let s = Rc::new(Rc::new(String::from("hello")));
    6. let s1 = s.clone(); // (1)
    7. //type_of(s1);
    8. let ps1 = (*s).clone(); // (2)
    9. //type_of(ps1);
    10. let pps1 = (**s).clone(); // (3)
    11. //type_of(pps1);
    12. }

    在以上的代码中,位置(1)处,s1的类型为Rc>,位置(2)处,ps1的类型为Rc,位置(3)处,pps1的类型为String。

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

    1. fn main() {
    2. let s = String::new();
    3. match &s {
    4. "" => {}
    5. _ => {}
    6. }
    7. }

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

    1. mismatched types:
    2. expected `&collections::string::String`,
    3. 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 的调用,简化代码。

  • 相关阅读:
    Go实现随机、轮训、权重、哈希负载均衡
    Codeforces Round #760 (Div. 3) D. Array and Operations
    Hutool基本用法介绍
    国外LEAD美国简称对照表
    Qt : 在QTreeWidget中添加自定义右键菜单
    【Linux进程】进程等待 与 进程替换 原理与函数使用
    IOS object-c大屏图表 PNChart 折线图 曲线图
    Nginx
    Java基础进阶多线程-线程安全和synchronized关键字
    gateway整合sentinel限流
  • 原文地址:https://blog.csdn.net/hebiwen95/article/details/126248246