越是了解Rust,你就会发现其中的言语方面设计的精细,比如这篇文章要讲的Cow。

标准库定义Cow为智能指针,目的是为了避免程序在内存操作上多余的复制拷贝数据。具体介绍可以看看标准库的文档。

定义

pub enum Cow<'a, B> 
where
    B: 'a + ToOwned + 'a + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Cow的定义咋一看有点吓人,但可以细分下,其实也还好。

  • Cow的泛型参数。包含一个生命周期 'a 和 类型 B
  • B的类型签名。可以看到B必须实现了了两个trait: ToOwned + ?Sized,还有一个生命周期 'a, 类型 B 的引用生命周期不小于 'a.
  • ToOwned。这个可以克隆数据到所有者。但有人肯定会问了,为什么不直接用 Clone。这二者还是有区别的,Clone 是从 &T 到 T,但ToOwned要强大些,可以从任意借用类型构建新类型实例。举个例子,
let s = "clone"; // str

let a: String = s.to_string(); // work

let b: String = s.clone(); // not work, str to String

let d: String = s.to_owned(); // work

可以看出不能从 str 克隆数据到 String,因为不是 &T 到 T,但ToOwned可以。

  • ?Sized. 这个很好理解,允许在运行时决定类型的内存大小。
  • Cow里面包含两个变量值,要么借用(borrow),要么克隆数据给新的所有者(clone on write)。

例子

假如有个数组里包含了一些字符串,有些字符串有统一的前缀名,有些没有,也有可能全部都有或者全部没有。如果没有这个前缀名,那就插入这个前缀名。并且你不想修改输入的字符串,可能留作后用。

按照需求,函数签名和实现可能是这样的。

fn insert_prefix_clone<'a>(strs: impl IntoIterator<Item=&'a String>, prefix: &'a str) -> Vec<String> {
    strs.into_iter().filter_map(|s| 
        match s.starts_with(prefix) {
            true => Some(s.clone()),
            false => Some(String::with_capacity(prefix.len() + s.len()) + prefix + s),
        })
    .collect::<Vec<String>>()
}

但这样做虽然能达到目的,但其实你的数组里可能包含着不少的已经拥有前缀名的字符串了,但函数也一股脑地拷贝了,这并没有做到rust里提倡的 zero cost。

所以这个时候,需要插入前缀名的字符串就拷贝,不需要就返回引用,这就节省了多余的内存开辟。

这就是Cow的用处了。所以用了Cow,这个函数的签名和实现大概如此:

fn insert_prefix_cow<'a, T>(strs: T, prefix: &'a str) -> Vec<Cow<'a, String>> where T: IntoIterator<Item=&'a String> {
    strs.into_iter().filter_map(|s| 
        match s.starts_with(prefix) {
            true => Some(Cow::Borrowed(s)),
            false => Some(Cow::Owned(String::with_capacity(prefix.len() + s.len()) + prefix + s)),
        }
    ).collect::<Vec<Cow<'a, String>>>()
}

用了Cow来封装数据,不需要插入前缀名的字符串,直接返回引用,二需要插入前缀名的字符串返回新的值。

那我们来细看下内存地址的有无变化。

let strs = vec!["row_rust".to_string(), "rust".to_string()];
let p = "row_";
let fixed = insert_prefix_cow(&strs, &p);

let s0 = &strs[0]; // 第一个元素已经有指定前缀名了
let f0 = &*f[0]; // Cow实现了Deref,所以可以直接解引用。

println!("source addr: {:?}", s0 as *const String); // 0x55aca68ac0
println!("cow addr: {:?}", f0 as *const String); //    0x55aca68ac0,地址相同

let s1 = &strs[1]; // 第二个元素插入了前缀名
let f1 = &*f[1];

println!("source addr: {:?}", s1 as *const String); // 0x55aca68ad8
println!("cow addr: {:?}", f1 as *const String); //    0x55aca68b88,地址已经发生了变化

可以看出,如果字符串包含了指定的前缀名,那么返回的数组里一部分字符串就指向原来的地址,也能看到地址确实没有变化,但如果插入了前缀名,那就意味着需要开辟新内存,也可以从结构看出,返回结果确实是指向了新的地址。

说了这么多,那我们来看下性能会如何?

Benchmark

Tips: 这个需要在nightly rust里面测试,我的测试环境是rust 1.38。

#![feature(test)]
extern crate test;
use test::Bencher;

// ...

#[bench]
fn test_cow(b: &mut Bencher) {
    let mut c = vec!["cow_rust".to_string(); 1024];
    let mut f = vec!["rust".to_string(); 1024];
    c.append(&mut f);
    let p = "cow_";
    b.iter(|| insert_prefix_cow(&c, &p));
}

#[bench]
fn test_clone(b: &mut Bencher) {
    let mut c = vec!["cow_rust".to_string(); 1024];
    let mut f = vec!["rust".to_string(); 1024];
    c.append(&mut f);
    let p = "cow_";
    b.iter(|| insert_prefix_clone(&c, &p));
}

看看测试结果。

running 2 tests
test test_clone ... bench:     218,206 ns/iter (+/- 3,479)
test test_cow   ... bench:     178,341 ns/iter (+/- 1,487)

可以看出,使用了Cow的函数还是比较明显快于直接克隆的函数,而且,数据量越大,差距越明显。

结语

Cow 提供了更为精细的内存控制,对于数据能够尽可能地重复使用,这都反映了rust的精心设计。

更多的使用和介绍,参考标准库文档。

Enjoy it!