这篇文章我们讨论 Rust 中的 Cow<T> 这个智能指针,它有什么作用?什么是写时复制(Copy on write)? 最后通过源码来分析其是如何实现的。
Cow 的作用
比如有如下一个方法:
fn escape_html(html_input: &mut String) -> &String { /* … */ }
这个方法的作用是过滤 html 中的非法字符。可能有两种执行路径,一种是当中没有非法字符,一种是有我对其进行转义替换。不管哪一种,我都需要传入一个 &mut String 可变借用。
所以,我通过 Cow<T> 方式来优化,方法的签名修改如下:
fn escape_html<'a>(mut input: Cow<'a, str>) -> Cow<'a, str> { /* … */ }
这样,当没有需要替换的内容的时候,就不会发生 Clone 和内存分配,使用的是只读引用。否则会通过 .into_owner() 或者 .to_mut() 方法获取所有权,写的时候才会发生内存分配。所以:
&mut String适用于必须修改且调用者愿意交出可变借用的场景;Cow<'a, str>适用于有时候读多写少且复用借用的场景,避免了无谓的 clone 操作。
使用Cow<T> 让程序可以更少的内存占用,更灵活的接口设计。
Cow 实现了 deref trait
Cow<T> 实现了 deref trait,可以让我们直接调用被包裹的类型 T 当中的方法,而不用手动解包。例如下面这个例子:
use std::borrow::Cow;
fn main() {
let borrowed_str: Cow<str> = Cow::Borrowed("Hello World!");
println!("length is {}", borrowed_str.len());
}
/**
* length is 12
*/
大部分的 Smart Pointer 都实现了
dereftrait,比如Box<T>、Rc<T>、Pin<T>等。
源码分析
我们来透过源码看看它是如何实现的吧:
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Cow"]
pub enum Cow<'a, B: ?Sized + 'a>
where
B: ToOwned,
{
/// Borrowed data.
#[stable(feature = "rust1", since = "1.0.0")]
Borrowed(#[stable(feature = "rust1", since = "1.0.0")] &'a B),
/// Owned data.
#[stable(feature = "rust1", since = "1.0.0")]
Owned(#[stable(feature = "rust1", since = "1.0.0")] <B as ToOwned>::Owned),
}
首先,它是一个 enum 类型,有两个 value,分别是 Borrowed 和 Owned ,所以要求 B 必须实现了 ToOwned 这个 trait。ToOwned 的作用是将一个借用类型的数据转换为了拥有所有权的数据,简单来说就是从“借用到拥有”(&T 到 T::Owned )。标准库中的 str 的 ToOwned 实现就是 String。
我们重点来关注,其写时复制这部分,当调用 to_mut() 方法的时候,源码如下:
#[stable(feature = "rust1", since = "1.0.0")]
pub fn to_mut(&mut self) -> &mut <B as ToOwned>::Owned {
match *self {
// 如果是 Borrowed,会产生 clone 的开销
Borrowed(borrowed) => {
// clone 数据
*self = Owned(borrowed.to_owned());
// 再次判断,理论上只会进入 Owned
match *self {
Borrowed(..) => unreachable!(),
Owned(ref mut owned) => owned,
}
}
// 如果已经是 Owner,则直接返回,没有 clone
Owned(ref mut owned) => owned,
}
}
可以看到,只有在 Borrowed 的情况下,才会调用 to_owned() 方法获取所有权,发生 clone。
Rc::make_mut() 和 Arc::make_mut()
Rc::make_mut() 、 Arc::make_mut() 和 Cow::<T> 一样都能够实现 Copy on write 的语义。规则如下:
- 当引用计数为 1 的时候,直接返回可变引用;
- 当引用计数大于 1 的时候,克隆数据返回新数据的独占的可变引用。
看如下例子:
use std::rc::Rc;
let mut data = Rc::new(String::from("hello"));
let data2 = Rc::clone(&data);
let s = Rc::make_mut(&mut data); // 此时会 clone,因为有多个引用
s.push_str(" world");
解释这里例子:
let mut data = ...:此时,data的引用计数是 1;let data2 = Rc...:此时,data和data2都指向同一份 String,引用计数是 2;let s = Rc::make_mut(...:此时,因为data的引用计数是 2,所以无法直接返回可变引用,而是 clone 了一份数据,让data指向新的Rc<String>实例,此时data的引用计数是 1,data2依然指向原来的数据,引用计数也是 1。s是data内部String的可变引用(&mut String);- 此时
data2和data分别独占一份 String,互不影响。
Arc 用于多线程场景,而 Rc 仅限单线程。
总结
Cow<'a, T> 既可以是借用的(Borrowed),也可以是拥有所有权的(Owned),它根据实际需要在二者之间切换。一般用在读多写少的处理流程(如配置解析、文本处理等),适用于零拷贝优化的场景下。作为函数参数时,如何兼容传入借用和拥有所有权的值。