Rust的 Deref 运算符
http://mp.weixin.qq.com/s/G28XE1rfX0nT6zIi86ji0Q
原创 2016-07-16 Rust编程公众号 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<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


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: Rust的 Deref 运算符相关推荐

  1. [rust] Rust与C++20编码习惯对照

    Rust 文章目录 Rust 差异 所有权 字符串 类型结构 生命周期 常量 基本类型 构造类型 String Vec HashMap File 泛型 Trait 编译时多态 运行时多态 函数 Lam ...

  2. Rust 基础(三)

    六.枚举和模式匹配 在本章中,我们将研究enumeration,也称为enum. 枚举允许通过枚举可能的变体来定义类型. 首先,将定义和使用枚举来展示枚举如何与数据一起编码意义.接下来,将探索一个特别 ...

  3. 8万字带你入门Rust

    Rust ?? 学习建议: 先从 整体出发,不要让自己陷入到细节中去 和自己已知的知识建立联系 rust 和go一样采用 组合的手段实现代码复用,不要深思为什么不是继承 学会阅读源码,从源码中学习 R ...

  4. gm怎么刷东西 rust_Rust语言:解引用详述,搞不明白这个概念,趁早放弃Rust

    Rust是内存安全的,对新手来说,最大的困难是可恶的编译器,在其他语言上面叱咤风云,偏偏被Rust搞到崩溃.所以,大家都戏谑道,Rust是面向编译器编程. 和编译器做斗争的过程中,遇到最多的是,变量所 ...

  5. Rust实战系列-基本语法

    本文是<Rust in action>学习总结系列的第二部分,更多内容请看已发布文章: 一.Rust实战系列-Rust介绍 " 主要介绍 Rust 的语法.基本类型和数据结构,通 ...

  6. Rust 五分钟了解,三十分种入门

    Rust 快速入门 初始化项目 基础 变量 - 常量 数据类型 函数 注释 控制流 所有权 移动 克隆 所有权与函数 返回值与作用域 引用与借用 可变引用 Slice 类型 其他类型的 slice 结 ...

  7. 回溯 Rust 2020:正成为最受欢迎的编程语言

    [CSDN 编者按]在 2020 年,Rust 的 Github Star 数达到了 51K,Reddit Fans 则涨至 125 K,全年合并 PR 有 8114 个.这些数据无不在说明,Rust ...

  8. RUST直接升钢指令_[译]参照TypeScript学习Rust-part-1

    [译]参照TypeScript学习Rust-1 · 前端在线​regx.vip 对于前端,笔者比较认可Rust作为前端开发技术栈投资的,本文系列翻译旨在分享.学习Rust这门语言. Rust常常被认为 ...

  9. Redox随笔(2)-用Rust语言编写的类UNIX操作系统

    与其他操作系统相比,Redox如何 我们与其他操作系统有很多共同之处. 由于 Redox syscall接口是Unix-y.例如,我们有open, pipe, pipe2, lseek, read, ...

  10. rust 编程入门_面向初学者的Rust –最受欢迎的编程语言入门

    rust 编程入门 Rust has been voted Stack Overflow's most loved programming language for five years in a r ...

最新文章

  1. 26岁应届博士被聘985博导!入职半年实现学院顶会论文零的突破
  2. C#开发Unity游戏教程之游戏对象的行为逻辑方法
  3. Flink从入门到精通100篇(十一)-Java SPI 机制在 Flink SQL 中的应用
  4. VError - Found 0 matching services的根源分析
  5. [CareerCup] 4.7 Lowest Common Ancestor of a Binary Search Tree 二叉树的最小共同父节点
  6. MySQL数据库之MyISAM与InnoDB的区别
  7. sql server一对多怎么查询_Vlookup函数查找最后一个值和一对多查询
  8. SQLite连接C#笔记
  9. Linux音频驱动-WAV文件格式分析
  10. Jeecg-Boot前后端分离版
  11. 通达信公式-当天成交量不大于百日均成交量比例
  12. Ant Design - Anchor
  13. C语言编程基础,手机购物程序的设计
  14. vue html if,vue中v-if使用方法详解
  15. 联合几位大佬给大家送110本技术书籍!包邮到家!!
  16. Matlab中table类型使用技巧
  17. 【C语言知识梳理之分支语句】
  18. python爬取视频自动播放_求助该网站如何让它能自动播放下一个视频。。。醉了,要挂80个课时...
  19. 数字图像频谱的中心化
  20. centos服务器部署

热门文章

  1. HTML fieldset 标签
  2. 8天玩转并行开发——第二天 Task的使用
  3. Recovery dropped Procedure
  4. Springboot druid 监控sql语句
  5. rabbitmq4-工作队列及公平分发模式
  6. unity3D与网页的交互
  7. Textarea自动换行如何设置
  8. C# 判断输入的字符是不是数字
  9. 做人做得最失败的一次
  10. 执行数据库命令Command对象——ADO.NET学习应用笔记之三