rust语言基础学习: 写时克隆智能指针Cow
2020-07-27
昨天学习了Rust中与借用数据相关的三个trait: Borrow
, BorrowMut
和ToOwned
。
理解了这三个trait之后,今天来学习Rust中能够实现写时克隆的智能指针Cow<'a B>
。
1.Cow的定义 #
Cow是Rust提供的用于实现写时克隆(Clone on write)的智能指针。
Cow的定义如下:
1pub enum Cow<'a, B>
2where
3 B: 'a + ToOwned + ?Sized,
4 {
5 Borrowed(&'a B),
6 Owned(<B as ToOwned>::Owned),
7}
从Cow的定义看,它是一个enum,包含一个对类型B的只读引用,或者包含一个拥有类型B的所有权的数据。 使用Cow使我们在返回数据时提供了两种可能: 要么返回一个借用的数据(只读),要么返回一个拥有所有权的数据(可读写)。
Cow trait的泛型参数约束比较复杂,下面详细介绍一下:
pub enum Cow<'a, B>
中的'a
是生命周期标注,表示Cow是一个包含引用的enum。泛型参数B需要满足'a + ToOwned + ?Sized
。即当Cow内部类型B的生命周期为’a时,Cow自己的生命周期也是’a。- 泛型参数B除了生命周期注解’a外,还有
ToOwned
和?Sized
两个约束 ?Sized
表示B是可变大小类型ToOwned
表示可以把借用的B数据克隆出一个拥有所有权的数据- 这个enum里的
Borrowed(&'a B)
表示返回借用数据是B类型的引用,引用的生命周期为’a - 因为B满足ToOwned trait,所以
Owned(<B as ToOwned>::Owned)
中的<B as ToOwned>::Owned
表示把B强制转换成ToOwned,并访问ToOwned内部的关联类型Owned
2.智能指针Cow #
了解了Cow这个用于写时克隆的智能指针的定义,它在定义上是一个枚举类型,有两个可选值:
Borrowed
用来包裹对象的引用Owned
用来包裹对象的所有者
下面从智能指针的角度来学习Cow。
在开始学习之前,先回顾一下智能指针的特征:
- 大多数情况下智能指针具有它所指向数据的所有权
- 智能指针是一种数据结构,一般使用结构体实现
- 智能指针数据类型的显著特征是实现Deref和Drop trait
当然,上面智能指针的特征都不是强制的,我们来看一下Cow做为智能指针是否有上面的这些特征:
- Cow枚举的Owned的可选值,可以返回一个拥有所有权的数据
- Cow作为智能指针在定义上是使用枚举类型实现的
- Cow实现的Deref trait,Cow没有实现Drop trait
我们知道,如果一个类型实现了Deref trait,那么就可以将类型当做常规引用类型使用。
下面是Cow对Deref trait的实现:
1impl<B: ?Sized + ToOwned> const Deref for Cow<'_, B>
2where
3 B::Owned: ~const Borrow<B>,
4{
5 type Target = B;
6
7 fn deref(&self) -> &B {
8 match *self {
9 Borrowed(borrowed) => borrowed,
10 Owned(ref owned) => owned.borrow(),
11 }
12 }
13}
在实现上很简单,match表达式中根据self是Borrowed还是Owned,分别取其内容,然后生成引用:
- 对于Borrowed选项,其内容就是引用
- 对于Owned选项,其内容是泛型参数B实现ToOwned中的关联类型Owned,而Owned是实现Borrow trait的,所以owned.borrow()可以获得引用
Cow<'a, B>
通过对Deref trait的实现,就变得很厉害了,因为智能指针通过Deref的实现就可以获得常规引用的使用体验。
对Cow<'a, B>
的使用,在体验上和直接&B
基本上时一致的。
通过函数或方法传参时Deref强制转换(Deref coercion)功能,可以使用Cow<'a, B>
直接调用B的不可变引用方法(&self)。
例1:
1use std::borrow::Cow;
2
3fn main() {
4 let hello = "hello world";
5 let c = Cow::Borrowed(hello);
6 println!("{}", c.starts_with("hello"));
7}
例1中变量c使用Cow包裹了一个&str
引用,随后直接调用了str的start_with方法。
3.Cow的方法 #
接下来看一下智能指针Cow都提供了哪些方法供我们使用。
pub fn into_owned(self) -> <B as ToOwned>::Owned
: into_owned
方法用于抽取Cow所包裹类型B的所有者权的数据,如果它还没有所有权数据将会克隆一份。
在一个Cow::Borrowed
上调用into_owned
,会克隆底层数据并成为Cow::Owned
。在一个Cow::Owned
上调用into_owned不会发生克隆操作。
例2:
1use std::borrow::Cow;
2
3fn main() {
4 let s = "Hello world!";
5
6 // 在一个`Cow::Borrowed`上调用`into_owned`,会克隆底层数据并成为`Cow::Owned`。
7 let cow1 = Cow::Borrowed(s);
8
9 assert_eq!(cow1.into_owned(), String::from(s));
10
11 // 在一个`Cow::Owned`上调用into_owned不会发生克隆操作。
12 let cow2: Cow<str> = Cow::Owned(String::from(s));
13
14 assert_eq!(cow2.into_owned(), String::from(s));
15}
pub fn to_mut(&mut self) -> &mut <B as ToOwned>::Owned
: 从Cow所包裹类型B的所有者权的数据获得一个可变引用,如果它还没有所有权数据将会克隆一份再返回其可变引用。
例3:
1use std::borrow::Cow;
2
3fn main() {
4 let mut cow = Cow::Borrowed("foo");
5 cow.to_mut().make_ascii_uppercase();
6
7 assert_eq!(cow, Cow::Owned(String::from("FOO")) as Cow<str>);
8}
关于Cow的这2个方法:
- 调用
to_mut()
方法可以得到一个具有所有权的值的可变引用,注意在已经具有所有权的情况下,也可以调用to_mut但不会产生新的克隆,多次调用to_mut只会产生一次克隆 - 调用
into_owned()
方法可以得到具有所有权的值,如果之前Cow是Borrowed借用状态,调用into_owned将会克隆,如果已经是Owned状态,将不会克隆
4.Cow的使用场景 #
使用Cow主要用来减少内存的分配和复制,因为绝大多数的场景都是读多写少。使用Cow可以在需要些的时候才做一次内存复制,这样就很大程度减少了内存复制次数。
先来看官方文档中的例子。
例4:
1use std::borrow::Cow;
2
3fn main() {
4 fn abs_all(input: &mut Cow<[i32]>) {
5 for i in 0..input.len() {
6 let v = input[i];
7 if v < 0 {
8 // Clones into a vector if not already owned.
9 input.to_mut()[i] = -v;
10 }
11 }
12 }
13
14 // No clone occurs because `input` doesn't need to be mutated.
15 let slice = [0, 1, 2];
16 let mut input = Cow::from(&slice[..]);
17 abs_all(&mut input);
18
19 // Clone occurs because `input` needs to be mutated.
20 let slice = [-1, 0, 1];
21 let mut input = Cow::from(&slice[..]);
22 abs_all(&mut input);
23
24 // No clone occurs because `input` is already owned.
25 let mut input = Cow::from(vec![-1, 0, 1]);
26 abs_all(&mut input);
27}
最后再来看一下例子。
例5:
1use std::borrow::Cow;
2
3const SENSITIVE_WORD: &str = "bad";
4
5fn remove_sensitive_word<'a>(words: &'a str) -> Cow<'a, str> {
6 if words.contains(SENSITIVE_WORD) {
7 Cow::Owned(words.replace(SENSITIVE_WORD, ""))
8 } else {
9 Cow::Borrowed(words)
10 }
11}
12
13fn remove_sensitive_word_old(words: &str) -> String {
14 if words.contains(SENSITIVE_WORD) {
15 words.replace(SENSITIVE_WORD, "")
16 } else {
17 words.to_owned()
18 }
19}
20
21fn main() {
22 let words = "I'm a bad boy.";
23 let new_words = remove_sensitive_word(words);
24 println!("{}", new_words);
25
26 let new_words = remove_sensitive_word_old(words);
27 println!("{}", new_words);
28}
例5的需求是实现一个字符串敏感词替换函数,从给定的字符串替换掉预制的敏感词。
例子中给出了remove_sensitive_word
和remove_sensitive_word_old
两种实现,前者的返回值使用了Cow,后者返回值使用的是String。
仔细分析一下,很明显前者的实现效率更高。因为如果输入的字符串中没有敏感词时,前者Cow::Borrowed(words)
不会发生堆内存的分配和拷贝,后者words.to_owned()
会发生一次堆内存的分配和拷贝。
试想一下,如果例5的敏感词替换场景,是大多数情况下都不会发生替换的,即读多写少的场景,remove_sensitive_word实现中使用Cow作为返回值就在很多程度上提高了系统的效率。
5.总结 #
在设计和编写Rust代码时,使用Cow可以实现在需要时才进行内存的分配和拷贝,在读多写少的场景下,可以减少堆内存分配次数,很大程度提高系统的效率。