电子说
Rust中与借用数据相关的三个trait:Borrow
,BorrowMut
和ToOwned
。理解了这三个trait之后,再学习Rust中能够实现写时克隆的智能指针Cow<'a B>
。写时克隆(Copy on Write)技术是一种程序中的优化策略,多应用于读多写少的场景。主要思想是创建对象的时候不立即进行复制,而是先引用(借用)原有对象进行大量的读操作,只有进行到少量的写操作的时候,才进行复制操作,将原有对象复制后再写入。这样的好处是在读多写少的场景下,减少了复制操作,提高了性能。
Cow是Rust提供的用于实现 **写时克隆(Copy on Write)** 的智能指针。
定义如下:
pubenumCow<'a, B>whereB:'a+ToOwned+ ?Sized, {/// 用于包裹引用(通用引用) Borrowed(&'a B), /// 用于包裹所有者; Owned(::Owned),}
**
从Cow的定义看,它是一个enum,包含一个对类型B的只读引用,或者包含一个拥有类型B的所有权的数据。
可以看到Cow是一个枚举体,包括两个可选值,一个是“借用”(只读),一个是“所有”(可读写)。具体含义是:以不可变的方式访问借用内容,在需要可变借用或所有权的时候再克隆一份数据。
Cow trait的泛型参数约束比较复杂,下面详细介绍一下:
pub enum Cow<'a, B>
中的'a
是生命周期标注,表示Cow是一个包含引用的enum。泛型参数B需要满足'a + ToOwned + ?Sized
。即当Cow内部类型B的生命周期为’a时,Cow自己的生命周期也是’a。ToOwned
和?Sized
两个约束?Sized
表示B是可变大小类型ToOwned
表示可以把借用的B数据复制出一个拥有所有权的数据Borrowed(&'a B)
表示返回借用数据是B类型的引用,引用的生命周期为’aOwned(::Owned)
中的::Owned
表示把B强制转换成ToOwned,并访问ToOwned内部的关联类型Owned了解了Cow这个用于写时克隆的智能指针的定义,它在定义上是一个枚举类型,有两个可选值:
Borrowed
用来包裹对象的引用Owned
用来包裹对象的所有者Cow 在这里就是表示借用的和自有的,但只能出现其中的一种情况。
下面从智能指针的角度来学习Cow。先回顾一下智能指针的特征:
当然,上面智能指针的特征都不是强制的,我们来看一下Cow做为智能指针是否有上面的这些特征:
我们知道,如果一个类型实现了Deref trait,那么就可以将类型当做常规引用类型使用。
下面是Cow对Deref trait的实现:
impl
**
在实现上很简单,match表达式中根据self是Borrowed还是Owned,分别取其内容,然后生成引用:
Cow<'a, B>
通过对Deref trait的实现,就变得很厉害了,因为智能指针通过Deref的实现就可以获得常规引用的使用体验。对Cow<'a, B>
的使用,在体验上和直接&B
基本上时一致的。
通过函数或方法传参时Deref强制转换(Deref coercion)功能,可以使用Cow<'a, B>
直接调用B的不可变引用方法(&self)。
例1:
usestd::borrow::Cow;fnmain() {lethello ="hello world";letc = Cow::Borrowed(hello);println!("{}", c.starts_with("hello")); }
例1中变量c使用Cow包裹了一个&str
引用,随后直接调用了str的start_with方法。
接下来看一下智能指针Cow都提供了哪些方法供我们使用。
2个关键函数:
impl
pub fn into_owned(self) ->::Owned
:into_owned
方法用于抽取Cow所包裹类型B的所有者权的数据,如果它还没有所有权数据将会克隆一份。在一个Cow::Borrowed
上调用into_owned
,会克隆底层数据并成为Cow::Owned
。在一个Cow::Owned
上调用into_owned不会发生克隆操作。
**
例2:
usestd::borrow::Cow;fnmain() {lets ="Hello world!";// 在一个`Cow::Borrowed`上调用`into_owned`,会克隆底层数据并成为`Cow::Owned`。letcow1 = Cow::Borrowed(s);assert_eq!(cow1.into_owned(),String::from(s));// 在一个`Cow::Owned`上调用into_owned不会发生克隆操作。letcow2: Cow<str> = Cow::Owned(String::from(s));assert_eq!(cow2.into_owned(),String::from(s)); }
pub fn to_mut(&mut self) -> &mut::Owned
: 从Cow所包裹类型B的所有者权的数据获得一个可变引用,如果它还没有所有权数据将会克隆一份再返回其可变引用。
**
例3:
usestd::borrow::Cow;fnmain() {letmutcow = Cow::Borrowed("foo"); cow.to_mut().make_ascii_uppercase();assert_eq!(cow, Cow::Owned(String::from("FOO"))asCow<str>); }
使用Cow主要用来减少内存的分配和复制,因为绝大多数的场景都是读多写少。使用Cow可以在需要些的时候才做一次内存复制,这样就很大程度减少了内存复制次数。
先来看官方文档中的例子。
例4:
usestd::borrow::Cow;fnmain() {fnabs_all(input: &mutCow<[i32]>) {foriin0..input.len() {letv = input[i];ifv <0{// Clones into a vector if not already owned.input.to_mut()[i] = -v; } } }// No clone occurs because `input` doesn't need to be mutated.letslice = [0,1,2];letmutinput = Cow::from(&slice[..]); abs_all(&mutinput);// Clone occurs because `input` needs to be mutated.letslice = [-1,0,1];letmutinput = Cow::from(&slice[..]); abs_all(&mutinput);// No clone occurs because `input` is already owned.letmutinput = Cow::from(vec![-1,0,1]); abs_all(&mutinput); }
最后再来看一下例子。
例5:
usestd::borrow::Cow;constSENSITIVE_WORD: &str="bad";fnremove_sensitive_word<'a>(words: &'astr) -> Cow<'a,str> {ifwords.contains(SENSITIVE_WORD) { Cow::Owned(words.replace(SENSITIVE_WORD,"")) }else{ Cow::Borrowed(words) } }fnremove_sensitive_word_old(words: &str) ->String{ifwords.contains(SENSITIVE_WORD) { words.replace(SENSITIVE_WORD,"") }else{ words.to_owned() } }fnmain() {letwords ="I'm a bad boy.";letnew_words = remove_sensitive_word(words);println!("{}", new_words);letnew_words = remove_sensitive_word_old(words);println!("{}", new_words); }
例5的需求是实现一个字符串敏感词替换函数,从给定的字符串替换掉预制的敏感词。
例子中给出了remove_sensitive_word
和remove_sensitive_word_old
两种实现,前者的返回值使用了Cow,后者返回值使用的是String。仔细分析一下,很明显前者的实现效率更高。因为如果输入的字符串中没有敏感词时,前者Cow::Borrowed(words)
不会发生堆内存的分配和拷贝,后者words.to_owned()
会发生一次堆内存的分配和拷贝。
试想一下,如果例5的敏感词替换场景,是大多数情况下都不会发生替换的,即读多写少的场景,remove_sensitive_word实现中使用Cow作为返回值就在很多程度上提高了系统的效率。
Cow 的设计目的是提高性能(减少复制)同时增加灵活性,因为大部分情况下,多用于读多写少的场景。利用 Cow,可以用统一,规范的形式实现,需要写的时候才做一次对象复制。
全部0条评论
快来发表一下你的评论吧 !