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方法时会出现什么情况。

相关推荐
Ray Liang15 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解15 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
蚂蚁背大象16 小时前
Rust 所有权系统是为了解决什么问题
后端·rust
布列瑟农的星空17 小时前
前端都能看懂的rust入门教程(五)—— 所有权
rust
SimonKing19 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean19 小时前
Jackson View Extension Spring Boot Starter
java·后端
Seven9721 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java
皮皮林5511 天前
Java性能调优黑科技!1行代码实现毫秒级耗时追踪,效率飙升300%!
java
冰_河1 天前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化