Rust编程学习 - 如何理解Rust 语言提供了所有权、默认move 语义、借用、生命周期、内部可变性

注意:&*两个操作符连写跟分开写是不同的含义。以下两种写法是不同的:

js 复制代码
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()做的是把字符串深复制一份。示例如下:

js 复制代码
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表 达式中:

js 复制代码
fn main(){
  let s =String::new();
  match  &s{
    ""=>{}
    _=>{}
  }
{

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

js 复制代码
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 智能指针的用法:

js 复制代码
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);
}

编译运行,结果显示:

js 复制代码
$./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 的源码就可以知道:

js 复制代码
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 类型的定义如下所示:

js 复制代码
pub struct Rc<T:?Sized>{
_ptr:Shared<RCBox<T>>, }

其中RCBox 是这样定义的:

js 复制代码
struct RcBox<T:?Sized>{
  strong:Cell<usize>,
  weak:Cell<usize>,
  value:T,
}

其中Shared 类型我们暂时可以不用管它,当它是一个普通指针就好。目前它还没有稳定,后续可能设计上还会有变化,因此本书就不对它深究了。

同时,它实现了clone 和 Drop 这两个trait 。在clone 方法中,它没有对它内部的数据 实行深复制,而是将强引用计数值加1,如下所示:

js 复制代码
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 指针指向这块内存,它占用的内存才会被彻底释放。如下所示:

js 复制代码
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 object
  ptr::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方法时会出现什么情况。

相关推荐
tianyuanwo2 小时前
Rust开发完全指南:从入门到与Python高效融合
开发语言·python·rust
乐悠小码2 小时前
Java设计模式精讲---03建造者模式
java·设计模式·建造者模式
一个人的幽默2 小时前
聊一下java获取客户的ip
java
披着羊皮不是狼2 小时前
Spring Boot——从零开始写一个接口:项目构建 + 分层实战
java·spring boot·后端·分层
charlie1145141913 小时前
从零开始理解 CSS:让网页“活”起来的语言2
前端·css·笔记·学习·选择器·样式表·原生
im_AMBER3 小时前
Leetcode 46
c语言·c++·笔记·学习·算法·leetcode
2401_860494703 小时前
Rust语言高级技巧 - RefCell 是另外一个提供了内部可变性的类型,Cell 类型没办法制造出直接指向内部数据的指针,为什么RefCell可以呢?
开发语言·rust·制造
国服第二切图仔3 小时前
Rust开发实战之密码学基础——哈希计算与对称加密实战
rust·密码学·哈希算法
2301_796512523 小时前
Rust编程学习 - 内存分配机制,如何动态大小类型和 `Sized` trait
学习·算法·rust