这篇文章我们讨论 Rust 中的 Cow<T>
这个智能指针,它有什么作用?什么是写时复制(Copy on write)? 最后通过源码来分析其是如何实现的。
Cow 的作用
比如有如下一个方法:
rust
fn escape_html(html_input: &mut String) -> &String { /* ... */ }
这个方法的作用是过滤 html 中的非法字符。可能有两种执行路径,一种是当中没有非法字符,一种是有我对其进行转义替换。不管哪一种,我都需要传入一个 &mut String
可变借用。
所以,我通过 Cow<T>
方式来优化,方法的签名修改如下:
rust
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
当中的方法,而不用手动解包。例如下面这个例子:
rust
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 都实现了
deref
trait,比如Box<T>
、Rc<T>
、Pin<T>
等。
源码分析
我们来透过源码看看它是如何实现的吧:
rust
#[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()
方法的时候,源码如下:
rust
#[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 的时候,克隆数据返回新数据的独占的可变引用。
看如下例子:
rust
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
),它根据实际需要在二者之间切换。一般用在读多写少的处理流程(如配置解析、文本处理等),适用于零拷贝优化的场景下。作为函数参数时,如何兼容传入借用和拥有所有权的值。