Rust编程学习 - 为什么说Cow 代表的是Copy-On-Write, 即“写时复制技术”,它是一种高效的 资源管理手段

大家需要注意的是,这个self的类型是&Self,不是&mutSelf。但我们同时还需要使用这个共享引用self来修改引用计数的值。

所以这个成员必须是具有内部可变性的。反之,如果它们是普通的整数,那么我们就要 求使用&mutSelf类型来调用clone方法,然而一般情况下,我们都会需要多个Rc 指 针指向同一块内存区域,引用计数值是共享的。如果存在多个&mut 型指针指向引用计数值的 话,则违反了Rust 内存安全的规则。

因 此 ,Rc 智能指针的实现,必须使用"内部可变性"功能。 Cell 类型提供了一种类似 C++ 的 mutable 关键字的能力,使我们可以通过不可变指针修改复合数据类型内部的某一个 成员变量。

所以,我们可以总结出最适合使用"内部可变性"的场景是:当逻辑上不可变的方法的 实现细节又要求某部分成员变量具有可变性的时候,我们可以使用"内部可变性"。 Rc 内部 的引用计数变量就是绝佳的例子。

多个 Rc 指针指向的共享内存区域如果需要修改的话,也必须用内部可变性。如在下面 的例子中,如果我们需要多个Rc 指针指向一个Vec, 而且具备修改权限的话,那我们必须 用RefCell 把 Vec 包起来:

js 复制代码
use std::rc::Rc;
use std::cell::RefCell;
fn main(){
  let shared_vec:Rc<RefCell<Vec<isize>>>=Rc::new(RefCell::new(vec![1,2,3]));
  let shared1 =shared_vec.clone();
  let shared2 =shared1.clone();
  shared1.borrow_mut()·push(4);
  println!("{:?}",shared_vec.borrow());
  shared2.borrow_mut()·push(5);
  println!("{:?}",shared_vec.borrow());
}

Cow

在 C++ 语境中,Cow 代表的是Copy-On-Write, 即"写时复制技术"。它是一种高效的 资源管理手段。假设我们有一份比较昂贵的资源,当我们需要复制的时候,我们可以采用 "浅复制"的方式,而不需要重新克隆一份新的资源。而如果要修改复制之后的值,这时候 再执行深复制,在此基础上修改。因此,它的优点是把克隆这个操作推迟到真正需要"复制 并写操作"的时候发生。

在Rust 语境中,因为Copy 和 Clone 有比较明确的语义区分, 一般把Cow 解释为Clone- On-Write 。它对指向的数据可能"拥有所有权",或者可能"不拥有所有权"。

当它只需要对所指向的数据进行只读访问的时候,它就只是一个借用指针;当它需要写 数据功能时,它会先分配内存,执行复制操作,再对自己拥有所有权的内存进行写入操作。 Cow 在标准库中是一个enum:

js 复制代码
pub enum Cow<'a,B:?Sized a>where B:ToOwned {
  ///Borrowed data.
  Borrowed(&'a B),
  ///Owned             data.
  Owned(<B      as      ToOwned>::Owned)
}

它可以是Borrowed 或者Owned 两种状态。如果是Borrowed 状态,可以通过调用to_ mut 函数获取所有权。在这个过程中,它实际上会分配一块新的内存,并将原来Borrowed状 态的数据通过调用to_owned()方法构造出一个新的拥有所有权的对象,然后对这块拥有 所有权的内存执行操作 。

Cow 类型最常见的是跟字符串配合使用:

js 复制代码
use std::borrow::Cow;
fn remove_spaces<'a>(input:&'a str)->Cow<'a,str>{
if input.contains(''){
  let mut buf =String::with_capacity(input.len());
    for c in input.chars(){
    if c!=''{
      buf.push(c);
    }
  }
  return  Cow::Owned(buf);
}
return Cow::Borrowed(input); }
fn main(){
  let s1="no_spaces_in_string";
  let result1 =remove_spaces(s1);
  let s2 ="spaces in string";
  let result2 =remove_spaces(s2);
  println!("{}\n{}",result1,result2);
}

在这个示例中,我们使用Cow 类型最主要的目的是优化执行效率。remove_spaces函数的输入参数是&str 类型。如果输入的参数本来就不包含空格,那么我们最好是直接返 回参数本身,无须分配新的内存;如果输入参数包含空格,我们就只能在函数体内部创建一 个新的String 对象,用于存储去除掉空格的结果,然后再返回去。

这样一来,就产生了一个小矛盾,这个函数的返回值类型用&str 和 String类型 都 不 大 合 适 。

  • 如果返回类型指定为&str 类型,那么需要新分配内存的时候,会出现生命周期编译 错误 。
  • 因为函数内部新分配的字符串的引用不能在函数调用结束后继续存在 。
  • 如果返回类型指定为String 类型,那么对于那种不需要对输入参数做修改的情况, 有一些性能损失。因为输入参数&str 类型转为String 类型需要分配新的内存空间 并执行复制,性能开销较大。

这种时候使用Cow 类型就是不二之选。既能满足编译器的生命周期要求,也避免了无谓 的数据复制。Cow 类型,就是优秀的"零性能损失抽象"的设计范例。C++implementations obey the zero-overhead principle:What you don't use,you don't pay for And further:What you do use,you couldn't hand code any better.

由于Rust 中有这套所有权、生命周期的基础,在Rust 中使用Cow 这种类型是完全没有 风险的,任何可能的内存安全问题,编译器都可以帮我们查出来。所以,有些时候,自由和 不自由是可以相互转化的,语法方面的不自由,反而可能造就抽象水平的更自由。

Cow 类型还实现了Deref trait, 所以当我们需要调用类型T 的成员函数的时候,可以直 接调用,完全无须考虑后面具体是"借用指针"还是"拥有所有权的指针"。所以我们也可 以把它当成是一种"智能指针"。---

小结

Rust 中允许一部分运算符可以由用户自定义行为,即"操作符重载"。其中"解引用" 是一个非常重要的操作符,它允许重载。

而需要提醒大家注意的是,"取引用"操作符,如&、&mut, 是不允许重载的。因此,"取 引用"和"解引用"并非对称互补关系。*&T 的类型一定是T, 而 & *T的类型未必就是T。

更重要的是,读者需要理解,在某些情况下,编译器帮我们插入了自动deref 的调用, 简化代码。在 Deref 的基础上,我们可以封装出一种自定义类型,它可以直接调用其内部的其他类 型的成员方法,我们可以把这种类型称为智能指针类型。


熟悉C++ 的朋友应该知道,在C++ 中,如果引用计数智能指针出现了循环引用,就会 导致内存泄漏。而Rust 中也一样存在引用计数智能指针Rc, 那 么Rust 中是否可能制造出内 存泄漏呢?

下面我们来通过一步步的尝试,看看如何才能构造一个内存泄漏的例子。

内存泄漏

首先,我们设计一个 Node 类型,它里面包含一个指针,可以指向其他的Node 实例:

js 复制代码
struct Node{
  next:Box<Node>
}
``

接下来我们尝试一下创建两个实例,将它们首尾相连:
```js
fn main(){
  let node1 =Node{next:Box::new(..)}
}

到这里写不下去了, Rust 中 要 求 ,Box 指针必须被合理初始化,而初始化Box 的时候 又必须先传入一个Node 实例,这个Node 的实例又要求创建一个 Box 指针。这成了"鸡生 蛋蛋生鸡"的无限循环。

要打破这个循环,我们需要使用"可空的指针"。在初始化Node 的时候,指针应该是 "空"状态,后面再把它们连接起来。我们把代码改进,为了能修改node 的值,还需要使用 mut:

js 复制代码
struct Node{
next :Option<Box<Node>>

fn main(){
 let mut node1 =Box::new(Node{next:None     });
 let mut node2 =Box::new(Node{next:None     });
  node1.next   =Some(node2);
 node2.next   =Some(node1); 
}

编译,发生错误: "error:use of moved value:'node2'"。

从编译信息中可以看到,在node1.next =Some(node2); 这条语句中发生了move 语义,从此句往后,node2 变量的生命周期已经结束了。因此后面一句中使用node2 的时 候发生了错误。那我们需要继续改进,不使用node2, 换而使用node1.next, 代码改成 下面这样 :

js 复制代码
fn main(){
  let mut node1 =Box::new(Node{next:None });
  let mut node2 =Box::new(Node{next :None });
  nodel.next =Some(node2);
  match node1.next{
  Some(mut n)=>n.next =Some(node1),
  None =>{}
  }
}

编译又发生了错误,错误信息为: "error:use of partially moved value:`nodel'"。

这是因为在match 语句中,我们把node1.next 的所有权转移到了局部变量n 中,这 个 n 实际上就是node2 的实例,在执行赋值操作n.next =Some(node1)的过程中,编译器认为此时node1 的一部分已经被转移出去了,它不能再被用于赋值号的右边。

看来,这是因为我们选择使用的指针类型不对,Box 类型的指针对所管理的内存拥有所 有权,只使用Box 指针没有办法构造一个循环引用的结构出来。于是,我们想到使用Rc 指 针。同时,我们还用了Drop trait来验证这个对象是否真正被释放了:

js 复制代码
use std::rc::Rc;
struct {
  next :Option<Rc<Node>>
}
impl Drop for Node{
  fn drop(&mut self){
  println!("drop");
  }
}
fn main(){
  let	mut node1 =Node{next:None	};
  let	mut node2 =Node{next:None	};
  let	mut node3 =Node{next      :None	};
  nodel.next =Some(Rc::new(node2));
  node2.next =Some(Rc::new(node3));
  node3.next =Some(Rc::new(node1));
}
相关推荐
故里21302 小时前
学习前端记录(二)21-40
学习
编啊编程啊程2 小时前
【029】智能停车计费系统
java·数据库·spring boot·spring·spring cloud·kafka
hashiqimiya2 小时前
springboot后端的接口headers
java·spring boot·后端
懒羊羊不懒@2 小时前
JavaSe—集合框架、Collection集合
java·开发语言
霸道流氓气质2 小时前
Java中Stream使用示例-对实体List分组且保留原数据顺序并对分组后的每组内的数据进行部分业务逻辑修改操作
java·list
ThreeYear_s2 小时前
电力电子技术学习路径与FPGA/DSP技术结合方向(gemini生成)
学习·fpga开发
2301_795167202 小时前
玩转Rust高级应用 如何进行理解Refutability(可反驳性): 模式是否会匹配失效
开发语言·算法·rust
java1234_小锋3 小时前
Spring事件监听的核心机制是什么?
java·spring·面试