深入理解 Rust 中的写时复制

这篇文章我们讨论 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,分别是 BorrowedOwned ,所以要求 B 必须实现了 ToOwned 这个 trait。ToOwned 的作用是将一个借用类型的数据转换为了拥有所有权的数据,简单来说就是从"借用到拥有"(&TT::Owned )。标准库中的 strToOwned 实现就是 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... :此时,datadata2 都指向同一份 String,引用计数是 2;
  • let s = Rc::make_mut(... :此时,因为 data 的引用计数是 2,所以无法直接返回可变引用,而是 clone 了一份数据,让 data 指向新的 Rc<String> 实例,此时 data 的引用计数是 1,data2 依然指向原来的数据,引用计数也是 1。sdata 内部 String 的可变引用(&mut String);
  • 此时 data2data 分别独占一份 String,互不影响。

Arc 用于多线程场景,而 Rc 仅限单线程。

总结

Cow<'a, T> 既可以是借用的(Borrowed),也可以是拥有所有权的(Owned),它根据实际需要在二者之间切换。一般用在读多写少的处理流程(如配置解析、文本处理等),适用于零拷贝优化的场景下。作为函数参数时,如何兼容传入借用和拥有所有权的值。

相关推荐
UestcXiye4 小时前
Rust 学习笔记:关于 Cargo 的练习题
rust
love530love6 小时前
Windows 下部署 SUNA 项目:虚拟环境尝试与最终方案
前端·人工智能·windows·后端·docker·rust·开源
维维酱10 小时前
Rust - move 关键字
rust
UestcXiye11 小时前
Rust 学习笔记:使用自定义命令扩展 Cargo
rust
维维酱12 小时前
Rust - 线程
rust
唯有选择14 小时前
是时候用ED25519替代RSA了:Rust库`crypto_box`实践
安全·rust
UestcXiye14 小时前
Rust 学习笔记:Cargo 工作区
rust
UestcXiye17 小时前
Rust 学习笔记:使用 cargo install 安装二进制 crate
rust
寻月隐君18 小时前
解锁Rust代码组织:轻松掌握Package、Crate与Module
后端·rust·github
维维酱18 小时前
Rust - 引用循环
rust