Rust是内存安全的,对新手来说,最大的困难是可恶的编译器,在其他语言上面叱咤风云,偏偏被Rust搞到崩溃。所以,大家都戏谑道,Rust是面向编译器编程。
和编译器做斗争的过程中,遇到最多的是,变量所有权被move了,某个api需要传入可变引用而我却不知道如何获取。
只要你了解了如何解引用,如何做类型转换,这些问题都将迎刃而解。
什么是解引用
引用是对内存块的借用,Rust里每一个内存块都是有主人的,主人就是对内存拥有所有权的变量,没有主人的内存块我们称之为内存泄露了。
解引用是通过引用找到内存块真正的主人,然后你可以跟主人借一些不同类型的引用,比如从&mut T借成Pin。
这个主人可能被包了很多层,当你以为你找了主人,其实它只是个皮,所以会存在不断解引用的情况,你可能需要加很多个*,当然很多个*不符合设计美感。
Rust语法规定,同一块内存只能有一个可变引用,或者有多个不可变引用。这给习惯于C++或者Java等语言的编程人员造成了很大的困扰,违反了多年以来养成的编程习惯,特别的别扭,因为他们眼中元原本的理解是所有的引用都是可变引用。
Rust之所以这么规定,一个非常大的优点是避免了内存被多处修改的潜在隐患,避免了资源的复杂环境竞争,降低了程序的调试难度。
那么,程序编写过程中必然会在不同的函数块里调用同一块内存,所以引用的使用将会变得非常频繁,我们犯的错误大多也在此。
解引用的方法
解引用可以分为,自动解引用和手动解引用。
Rust为了减少某些场合下重复解引用导致的代码美观问题,在编译期做了一些智能识别功能,比如带有&T参数的函数被调用的时候,你传&&&......&&&T都可以自动解引用,直到符合函数的参数类型为止。
手动解引用,就是和其他语言类似,借用是&操作符,解引用是*操作符。
我们也可以通过自行实现Deref这个Trait来自定义解引用的最终目标是什么,而恰恰这个也是Rust语言最难的地方,你得了解每个类型是否实现了Deref,而Rust类型实在是太多了,连&T借用也算一个新的类型,&T是不能继承T的所有特性的。
如果想要学好Rust,对Deref不做深入的研究,将会死的很难看。
解引用进阶篇
1、手动实现Deref
Rust已经为所有的&T和&mut T的类型默认实现了简陋版的Deref,解引用就是得到T本身。
当然我们也可以给类型T本身添加一个Deref,这样T本身就可以被接引用,相当于T也是一个引用类型,其实T不是。
下面这个短暂的动态图片形象的阐述了Deref是如何自定义的,以及遇到类型不匹配的函数时,Rust是会自动解引用直到类型匹配为止。
2、以Pin
为例子,来谈谈解引用
如果P没有实现Deref,即P不可以解引用,那么Pin
是不能被*操作符操作的。
当P实现了Deref,那么*Pin
的解引用会先触发P解引用操作,得到P的Target的引用,再使用*操作符,最终的结果是P的Target被返回了。然而Pin
本身使用deref()得到的是&Target。
所有实现Deref的类型,使用deref函数得到的结果,肯定是一个引用类型。
*操作符为了更通用和方便,原理就是将被解引用的对象,先使用deref调用一下,再解引用。
项目中,最常遇见的是Pin,我们希望得到T的Target的可变引用,一般使用&mut **Pin。如果不了解它是怎么执行的,那么肯定会很疑惑,这个结果是啥。
3、Option如何得到T的Target引用
大多数情况下,我们得到的是Option,可是我们想调用的函数需要传入&T或者T的Target引用,这样就会很疑惑,该怎么得到这个&T呢?
这里也涉及到几个类型的deref函数的调用,首先是要把Option转化为Option,然后再将T转化为Target引用,如Option.as_ref().map(|s| { s.deref()}),或者直接使用Option自带的as_deref()。
4、Box是个特例
严格说Box这个语法在Rust里是特权一般的存在,各种小后门开的不亦乐乎。
Box在实现Deref这个Trait时,竟然使用的是&**语法,可是*操作符必须作用在实现Deref的类型上,这到底是先有的鸡,还是先有的蛋呢?
所以,不需要纠结这种被Rust开洞的特例,会使用即可。
写在最后
*操作符的特性是,先将作用对象进行deref得到一个引用,再将&引用清除。如果只是想得到一个引用,那么使用&*或者deref()。
如果不好理解,可以这么去记忆:*是用来消除&和box符号的,当发现目标没有&和box,就会调用deref函数去生成带有&符号的类型,然后就可以愉快的消除了。
掌握好这个技巧,在遇到各种&和*操作符的时候,才不至于被迷惑,找不到深层次的代码的实现原理。如果你对技术也有追求,对现实迷茫,请关注我,带你一起学习成长。