Rust编程学习 - 如何理解Rust 语言提供了所有权、默认move 语义、借用、生命周期、内部可变性
注意:&*两个操作符连写跟分开写是不同的含义。以下两种写法是不同的:
fn joint(){let s =Box::new(String::new());let p=&*s;println!("{}{}",p,s);
}
fn separate(){let s =Box::new(String::new());let tmp =*s;let p=&tmp;println!("{}{}",p,s);
}
fn main(){joint();separate();
}
fn joint() 是可以直接编译通过的,而fn separate()是不能编译通过的。因 为编译器很聪明,它看到&*这两个操作连在一起的时候,会直接把&s 表达式理解为 s.deref(), 这时候p 只是s 的一个借用而已。而如果把这两个操作分开写,会先执行s 把内部的数据move 出来,再对这个临时变量取引用,这时候s 已经被移走了,生命周期已 经结束。
同 样 的 ,let p =&{*s};这种写法也编译不过。这个花括号的存在创建了一个临时 的代码块,在这个临时代码块内部先执行解引用,同样是move 语 义 。
从这里我们也可以看到,默认的“取引用”、“解引用”操作是互补抵消的关系,互为逆 运算。但是,在Rust 中,只允许自定义“解引用”,不允许自定义“取引用”。如果类型有 自定义“解引用”,那么对它执行“解引用”和“取引用”就不再是互补抵消的结果了。先& 后 * 以 及 先 * 后 & 的 结 果 是 不 同 的 。
有时候需要手动处理
如果智能指针中的方法与它内部成员的方法冲突了怎么办呢?编译器会优先调用当前最 匹配的类型,而不会执行自动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(); /1(2)//type_of(ps1);let pps1 =(**s).clone(); /1(3) //type_of(pps1);
}
在以上的代码中,位置(1)处s1 的类型为Rc<Rc>,位 置 ( 2 ) 处ps1 的 类型为Rc,位置(3)处pps1 的类型为String。一般情况下,在函数调用的时候,编译器会帮我们尝试自动解引用。但在某些情况下, 编译器不会为我们自动插入自动解引用的代码。以String 和 &str 类型为例,在match表 达式中:
fn main(){let s =String::new();match &s{""=>{}_=>{}}
{
这段代码编译会发生错误,错误信息为:
mismatched types:
expected &collections::string::String`,
found'&'static str
match后面的变量类型是&String, 匹配分支的变量类型为&'static str,这种情 况下就需要我们手动完成类型转换了。手动将&String 类型转换为&str 类型的办法如下。
-
1)matchs.deref()。这个方法通过主动调用deref() 方法达到类型转换的目的。 此时我们需要引入 Deref trait方可通过编译,即加上代码usestd::ops::Deref;。
-
2)match &s。我们可以通过s 运算符,也可以强制调用deref() 方法,与上面的 做法一样。
-
3)match s.as_ref()。这个方法调用的是标准库中的std::convert::AsRef方法,这个trait 存在于prelude中,无须手工引入即可使用。
-
4)match s.borrow()。这个方法调用的是标准库中的std::borrow::Borrow方法。要使用它,需要加上代码use std::borrow::Borrow;。
-
5)match &s[ ·.]。这个方案也是可以的,这里利用了String重载的Index操作。
智能指针
Rust 语言提供了所有权、默认move 语义、借用、生命周期、内部可变性等基础概念。 但这些并不是Rust 全部的内存管理方式,在这些概念的基础上,我们还能继续抽象、封装更 多的内存管理方式,而且保证内存安全。
引 用 计 数
到目前为止,我们接触到的示例中都是一块内存总是只有唯一的一个所有者。当这个变 量绑定自身消亡的时候,这块内存就会被释放。引用计数智能指针给我们提供了另外一种选 择:一块不可变内存可以有多个所有者,当所有的所有者消亡后,这块内存才会被释放。
Rust 中提供的引用计数指针有std::rc::Rc类型和std::sync::Arc类型。 Rc 类型和Arc 类型的主要区别是: Rc 类型的引用计数是普通整数操作,只能用在单线程 中 ;Arc 类型的引用计数是原子操作,可以用在多线程中。这一点是通过编译器静态检查保 证的。Arc 类型的讲解可以参见第四部分相关章节,本章主要关注Rc 类型。
首先我们用示例展示 Rc 智能指针的用法:
use std::rc::Rc;
struct Sharedvalue {value:i32
}
fn main(){let shared_value :Rc<Sharedvalue>=Rc::new(SharedValue{value :42 });let owner1 =shared_value.clone();let owner2 =shared_value.clone();println!("value :{}{}",owner1.value,owner2.value);println!("address :{:p}{:p}",&owner1.value,&owner2.value);
}
编译运行,结果显示:
$./test
value:4242
address :0x13958abdf200x13958abdf20
这说明,owner1 owner2 里面包含的数据不仅值是相同的,而且地址也是相同的。这 正是Rc 的意义所在。从示例中可以看到,Rc 指针的创建是调用Rc::new 静态函数,与Box 类型一致(将来 会允许使用box 关键字创建)。如果要创建指向同样内存区域的多个Rc 指针,需要显式调用 clone 函数。请注意,Rc 指针是没有实现Copy trait的。如果使用直接赋值方式,会执行 move 语义,导致前一个指针失效,后一个指针开始起作用,而且引用计数值不变。如果需要创造新的Rc 指针,必须手工调用clone()函数,此时引用计数值才会加1。当某个Rc 指 针失效,会导致引用计数值减1。当引用计数值减到0的时候,共享内存空间才会被释放。
这没有违反我们前面讲的“内存安全”原则,它内部包含的数据是“不可变的”,每个 Rc 指针对它指向的内部数据只有读功能,和共享引用&一致,因此,它是安全的。区别在 于,共享引用对数据完全没有所有权,不负责内存的释放,Rc 指针会在引用计数值减到0的 时候释放内存。Rust 里面的Rc 类型类似于C++ 里面的shared_ptr类型, 且强制不可为空。
从示例中我们还可以看到,使用Rc 访问被包含的内部成员时,可以直接使用小数点语 法来进行,与T &T Box类型的使用方法一样。原因我们在前面已经讲过了,这是因为 编译器帮我们做了自动解引用。我们查一下Rc 的源码就可以知道:
impl<T:?Sized>Deref for Rc<T>{
type Target =T;
#[inline(always)]fn deref(&self)->&T{&self.inner().value}
}
可 见 ,Rc 类型重载了“解引用”运算符,而且恰好Target 类型指定的是T。这就意味 着编译器可以将Rc 类型在必要的时候自动转换为&T类型,于是它就可以访问T 的成员 变量,调用T 的成员方法了。因此,它可以被归类为“智能指针”。
下面我们继续分析Rc 类型的实现原理。它的源代码在src/liballoc/rc.rs中 ,Rc 类型的定义如下所示:
pub struct Rc<T:?Sized>{
_ptr:Shared<RCBox<T>>, }
其中RCBox 是这样定义的:
struct RcBox<T:?Sized>{strong:Cell<usize>,weak:Cell<usize>,value:T,
}
其中Shared 类型我们暂时可以不用管它,当它是一个普通指针就好。目前它还没有稳定,后续可能设计上还会有变化,因此本书就不对它深究了。
同时,它实现了clone 和 Drop 这两个trait 。在clone 方法中,它没有对它内部的数据 实行深复制,而是将强引用计数值加1,如下所示:
impl<T:?Sized>Clone for Rc<T>{
#[inline]
fn clone(&self)->Rc<T>{self.inc_strong();Rc{ptr:self.ptr }}
}
fn inc_strong(&self){self.inner().strong.set(self.strong().checked_add(1).unwrap_or_else(||unsafe{abort()}));
}
在 drop 方法中,也没有直接把内部数据释放掉,而是将强引用计数值减1,当强引用 计数值减到0的时候,才会析构掉共享的那块数据。当弱引用计数值也减为0的时候,才说 明没有任何Rc/Weak 指针指向这块内存,它占用的内存才会被彻底释放。如下所示:
unsafe impl<#[may_dangle]T:?Sized>Drop for Rc<T>{
fn drop(&mut self){unsafe{ let ptr =self.ptr.as_ptr();self.dec_strong();if self.strong()==0{//destroy the contained objectptr::drop_in_place(self.ptr.as_mut());//remove the implicit"strong weak"pointer now that we've //destroyed the contents.self.dec_weak();if self.weak()==0{Heap.dealloc(ptr as *mut u8,Layout::for_value(&*ptr)); }
}
从上面代码中我们可以看到,Rc 智能指针所指向的数据,内部包含了强引用和弱引用的计数值。这两个计数值都是用Cell 包起来的。为什么这两个数字一定要用Cell 包 来呢?我们假设,如果不用Cell, 而是直接用usize的话,在执行clone方法时会出现什么情况。
