Cow<’a, B>: 写时拷贝

写时复制(Copy-on-write)有异曲同工之妙

Cow 是 Rust 下用于提供写时克隆(Clone-on-Write)的一个智能指针,它跟虚拟内存管 理的写时复制(Copy-on-write)有异曲同工之妙:

包裹一个只读借用,但如果调用者需 要所有权或者需要修改内容,那么它会 clone 借用的数据

定义

Cow定义

#![allow(unused)]

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

它是一个 enum,可以包含一个对类型 B 的只读引用,或者包含对类型 B 的拥有所有权的 数据。

具体约束说明见泛型数据结构示例

Cow定义用到ToOwned、Borrowed、Owned、ToOwned梳理

从ToOwned定义开始

想要理解 Cow trait,ToOwned trait 是一道坎: ToOwned定义 -> 关联类型用到Borrow -> Borrow是一个用到Borrowed的trait object

#![allow(unused)]

fn main() {
pub trait ToOwned {
    type Owned: Borrow<Self>;
    #[must_use = "cloning is often expensive and is not expected to have side effects"]
    fn to_owned(&self) -> Self::Owned;

    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

pub trait Borrow<Borrowed> where Borrowed: ?Sized {
    fn borrow(&self) -> &Borrowed;
}
}
  1. 首先,type Owned: Borrow 是一个带有关联类型的 trait
  2. 这里 Owned 是关联类型,需要使用者定义
  3. 这里 Owned 不能是任意类型,它必须满足 Borrow trait。

关联类型Owned的理解

type Owned: Borrow: 参考str对ToOwned trait的实现

#![allow(unused)]

fn main() {
impl ToOwned for str {
    type Owned = String;
    #[inline]
    fn to_owned(&self) -> String {
        unsafe { String::from_utf8_unchecked(self.as_bytes().to_owned()) }
    }

    fn clone_into(&self, target: &mut String) {
        let mut b = mem::take(target).into_bytes();
        self.as_bytes().clone_into(&mut b);
        *target = unsafe { String::from_utf8_unchecked(b) }
    }
}
}

对上面例子的type Owned 为 String如何理解?

  1. ToOwned 要求关联类型Owned实现Borrow, 而此刻实现 ToOwned 的主体是 str,所以 Borrow 是 Borrow

  2. Owned被定义为String,也就是说 String 要实现 Borrow

#![allow(unused)]
fn main() {
impl Borrow<str> for String {
    #[inline]
    fn borrow(&self) -> &str {
        &self[..]
    }
}
}

Borrow Trait理解: 匹配分发

为何 Borrow 要定义成一个泛型 trait 呢?

例子1:String不同借用方式


use std::borrow::Borrow;

fn main() {
    let s = "hello world!".to_owned();

    // 这里必须声明类型,因为 String 有多个 Borrow<T> 实现
    // 借用为 &String
    let r1: &String = s.borrow();
    // 借用为 &str
    let r2: &str = s.borrow();

    println!("r1: {:p}, r2: {:p}", r1, r2);
}

String 可以被借用为 &String,也可以被借用为 &str

例子2:Cow不同解引用方式

#![allow(unused)]

fn main() {
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
    type Target = B;

    fn deref(&self) -> &B {
        match *self {
            Borrowed(borrowed) => borrowed,
            Owned(ref owned) => owned.borrow(),
        }
    }
}

}

实现的原理很简单,根据 self 是 Borrowed 还是 Owned,我们分别取其内容,生成引用:

  1. 对于 Borrowed,直接就是引用;
  2. 对于 Owned,调用其 borrow() 方法,获得引用。

匹配分发

匹配分发:使用match匹配实现静态、动态分发之外的第三种分发

虽然 Cow 是一个 enum,但是通过 Deref 的实现,我们可以获得统一的 体验. 比如 Cow,使用的感觉和 &str / String 是基本一致的。 注意,这种根据 enum 的不同状态来进行统一分发的方法是第三种分发手段,另外还可以使用泛型参数 做静态分发和使用 trait object 做动态分发

关系图整理

Cow 和 Owned / ToOwned / Borrow / Borrowed 之间的关系示意图

type Owned: Borrow<Self>

Cow在需要时才进行内存分配拷贝

场景1:写时拷贝

写时拷贝

那么 Cow 有什么用呢?

  1. 显然,它可以在需要的时候才进行内存的分配和拷贝,在很多应用 场合,它可以大大提升系统的效率。
  2. 如果 Cow<’a, B> 中的 Owned 数据类型是一个需要 在堆上分配内存的类型,如 String、Vec 等,还能减少堆内存分配的次数。
  3. 相对于栈内存的分配释放来说,堆内存的分配和释放效率要低很多,其内部还 涉及系统调用和锁,减少不必要的堆内存分配是提升系统效率的关键手段。
  4. 而 Rust 的 Cow<’a, B>,在帮助你达成这个效果的同时,使用体验还非常简单舒服。

场景2:URL解析

举例使用Cow进行URL解析

use std::borrow::Cow;

use url::Url;
fn main() {
    let url = Url::parse("https://tyr.com/rust?page=1024&sort=desc&extra=hello%20world").unwrap();
    let mut pairs = url.query_pairs();

    assert_eq!(pairs.count(), 3);

    let (mut k, v) = pairs.next().unwrap();
    // 因为 k, v 都是 Cow<str> 他们用起来感觉和 &str 或者 String 一样
    // 此刻,他们都是 Borrowed
    println!("key: {}, v: {}", k, v);
    // 当修改发生时,k 变成 Owned
    k.to_mut().push_str("_lala");

    print_pairs((k, v));

    print_pairs(pairs.next().unwrap());
    print_pairs(pairs.next().unwrap());
}

fn print_pairs(pair: (Cow<str>, Cow<str>)) {
    println!("key: {}, value: {}", show_cow(pair.0), show_cow(pair.1));
}

fn show_cow(cow: Cow<str>) -> String {
    match cow {
        Cow::Borrowed(v) => format!("Borrowed {}", v),
        Cow::Owned(v) => format!("Owned {}", v),
    }
}

在解析 URL 的时候,我们经常需要将 querystring 中的参数,提取成 KV pair 来进一步使 用。 绝大多数语言中,提取出来的 KV 都是新的字符串,在每秒钟处理几十 k 甚至上百 k 请求的系统中,你可以想象这会带来多少次堆内存的分配。 但在 Rust 中,我们可以用 Cow 类型轻松高效处理它,在读取 URL 的过程中:

  1. 每解析出一个 key 或者 value,我们可以用一个 &str 指向 URL 中相应的位置,然后用 Cow 封装它
  2. 而当解析出来的内容不能直接使用,需要 decode 时,比如 “hello%20world”,我们 可以生成一个解析后的 String,同样用 Cow 封装它。

场景3:序列化/反序列化

举例serde使用Cow进行序列化/反序列化

use serde::Deserialize;
use std::borrow::Cow;

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct User<'input> {
    #[serde(borrow)]
    name: Cow<'input, str>,
    age: u8,
}

fn main() {
    let input = r#"{ "name": "Tyr", "age": 18 }"#;
    let user: User = serde_json::from_str(input).unwrap();

    match user.name {
        Cow::Borrowed(x) => println!("borrowed {}", x),
        Cow::Owned(x) => println!("owned {}", x),
    }
}