Rust的Cow类型有什么用?详解Cow及其用途

Rust的智能指针有哪些?大多数人都能马上答出Box<T>Rc<T>Arc<T>和在异步编程中很常见的Pin<P>等等。不过,有一个可能经常被大多数人遗忘的类型,它功能强大,利用好了可以节省很多复制开销;它就是这篇文章的主角:Cow<B>

什么是COW(Copy-On-Write)?

在开始之前,有必要先介绍一下COW(Copy-On-Write,写时复制)的概念。COW是一种用于资源管理的优化策略,在操作系统中应用非常广泛。COW的核心思想是当多个任务需要读取同一个资源(比如内存中的数据、文件)的时候,它们会共享同一份资源副本,而不是为每个任务复制一份资源副本。只有当某个任务需要修改这个资源时,才会为这个任务创建一份资源副本。

需要注意的是,上述的整个过程对任务(也就是程序员编写的用户程序)来说都是不可见的;对程序员来说,他并不知道他所使用的资源在发生写操作时才被真正地复制了一份,自始至终他仿佛就像在独占整份资源一样。

COW在文件系统、虚拟内存管理中都有非常成熟的应用;在编程语言中,也被广泛应用于优化字符串、集合的处理。

Cow:定义

Rust的Cow<B>是一个枚举类型,包含两个成员:BorrowedOwned。不过,我们几乎不会直接用到它的成员,因为Cow<B>实现了Deref特征,这使得我们可以通过Deref转换这一语法糖来便捷地直接使用Cow<B>中的内容。有关Deref转换可以阅读我之前的文章。

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

需要注意一下Cow的模板参数。Cow接受一个生命周期和一个类型B,其中类型B需要实现ToOwned特征;ToOwned特征的介绍可以看之前的文章,这里仅仅提一下所有实现了Clone的类型都会自动实现ToOwned自身。除此以外,成员Owned的内容类型不是类型B本身,而是类型BToOwned的目标类型(例如对str来说,这个类型是String)。

使用方法

这里是一段Cow<B>的简单使用范例:

use std::borrow::Cow;

fn main() {
    let foo = "Hello World";
    let mut bar: Cow<str> = Cow::from(foo);
    println!("{bar}");      // 这里没有发生复制
    
    bar.to_mut().push_str(" Rust");  // 这里发生了复制
    println!("{bar}");
    
    println!("{foo}");      // 原来的字符串foo仍然可用,而且没有变化
}

Cow的构造

Cow<B>是一个枚举,所以首先它是可以直接从它的成员BorrowedOwned来构造的:

use std::borrow::Cow;

fn main() {
    let str_ = "Hello World";
    let string = String::from("Hello World!");
    
    let foo: Cow<str> = Cow::Borrowed(str_);
    let bar: Cow<str> = Cow::Owned(string);
    
    // 这里string不再可用
    // println!("{string}");
}

除此以外,标准库中的五对实现了ToOwned的类型(str/String[T]/Vec<T>CStr/CStringOsStr/OsStringPath/PathBuf)也可以使用From::from来构造Cow<B>

use std::borrow::Cow;

fn main() {
    let str_ = "Hello World";
    let string = String::from("Hello World!");
    
    let foo: Cow<str> = Cow::from(str_);	// from -> Borrowed
    let bar: Cow<str> = Cow::from(string);	// from -> Owned
    
    // 这里string不再可用
    // println!("{string}");
}

使用From::from时,Rust会自动为我们匹配正确的类型(&'a str/String等),一般情况下推荐使用from来构造Cow,而不是手动指定Borrowed/Owned

deref和to_mut

前面提到过,Cow<B>实现了Deref<B>特征,这意味着我们不需要做任何操作就可以享受Deref转换的语法糖:

use std::borrow::Cow;

fn main() {
    let str1 = "Hello World";
    let cow: Cow<str> = Cow::from(str1);
    let str2: &str = &cow;  // 注意看,我们把&Cow<str>赋给了&str
    
    println!("{str2}"); // Hello World
    println!("{cow}");  // Hello World
    println!("{str1}"); // Hello World
}
use std::borrow::Cow;

fn main() {
    let str1 = "Hello World";
    let cow: Cow<str> = Cow::from(str1);
    
    cow.split(" ").for_each(|s|println!("{s}"));	// 使用str的方法split也不在话下
}

不过,Cow<B>并没有实现DerefMut;这意味着我们对Cow的修改不会影响到底层的内容,相反地,当我们试图修改Cow时,Cow会生成一个副本,并且修改这个拥有所有权的副本:

use std::borrow::Cow;

fn main() {
    let str1 = "Hello";
    let mut cow: Cow<str> = Cow::from(str1);
    
    cow += " World";
    
    println!("cow = {cow}");	// cow = Hello World
    println!("str1 = {str1}");	// str1 = Hello 
}

我们可以多加一点输出代码,来看看具体发生了什么:

#![feature(cow_is_borrowed)]
use std::borrow::Cow;

fn main() {
    let str1 = "Hello";
    let mut cow: Cow<str> = Cow::from(str1);
    
    println!("cow = {cow}, borrowed = {}", cow.is_borrowed());	// cow = Hello, borrowed = true
    
    cow += " World";
    
    println!("cow = {cow}, borrowed = {}", cow.is_borrowed());	// cow = Hello World, borrowed = false
    println!("str1 = {str1}");									// str1 = Hello
}

修改了cow变量后,它不再处于借用状态,而是拥有了这段字符串的所有权——这也是它能够安全地修改这段字符串的关键。


除了直接对Cow<str>使用str中实现的方法来修改字符串之外,还可以使用to_mut()来获取&String来使用String中实现的方法来修改字符串:

use std::borrow::Cow;

fn main() {
    let str1 = "Hello";
    let mut cow: Cow<str> = Cow::from(str1);
    
    cow.to_mut().push_str(" World");
    
    println!("cow = {cow}");	// cow = Hello World
    println!("str1 = {str1}");	// str1 = Hello 
}

再重复一遍:使用to_mut()修改和直接修改Cow<B>的不同在于,to_mut()返回的是&mut <B as ToOwned>::Owned(例如String),可以使用BOwned类型(例如String)中额外实现的方法(例如String::push_str);修改Cow<B>的时候,只能使用B中实现的方法(例如上面的+=,也就是str::add_assign)。

消费Cow

在不再需要使用Cow,或者想要完整取得Cow中的对象的所有权的时候,我们可以使用Cow::into_owned方法来消费掉Cow。方法返回的是BOwned类型(例如String)。

use std::borrow::Cow;

fn main() {
    let str1 = "Hello";
    let mut cow: Cow<str> = Cow::from(str1);
    
    cow.to_mut().push_str(" World");
    
    let owned: String = cow.into_owned();
    
    println!("{owned}");    // Hello World
    println!("{str1}");	    // Hello 
}

在消费掉Cow之后,Cow将不再可用,但它之前借用的原数据不受影响。

用途

说了这么多,Cow到底有什么用呢?少复制几次数据真的那么重要吗?让我们看看标准库中的String::from_utf8_lossy方法吧。

String::from_utf8_lossy是一个把一个字节切片(&[u8])按照UTF-8转换成&str的方法,并且会用“�”字符来替换掉字节切片中UTF-8不支持的字符。举个例子:

// 不包含错误字节的情况
fn main() {
    let hello = vec![72, 69, 76, 76, 79];
    let hello = String::from_utf8_lossy(&hello);
    assert_eq!("HELLO", hello);
}

以及:

// 包含错误字节的情况
fn main() {
    let input = b"Hello \xF0\x90\x80World";
    let output = String::from_utf8_lossy(input);
    assert_eq!("Hello �World", output);
}

现在假设我们是Rust标准库API的设计师,我们要为from_utf8_lossy方法选择一个恰当的返回类型。

返回&str可以吗?

最直接的想法就是返回一个&str,就像这样:

fn from_utf8_lossy<'a>(v: &'a [u8]) -> &'a str {
   todo!()
}

这种方案可以吗?仔细想想,当字节切片中有UTF-8中不支持的错误字符时,错误字符需要被替换成“�”;直接返回&str的话是做不了对字符串内容的修改的。

返回String呢?

顺着刚才的思路,因为我们可能需要修改字符串,所以我们就需要返回&str的栈上类型String,合情合理:

fn from_utf8_lossy(v: &[u8]) -> String {
   todo!()
}

不过,另一个问题冒出来了:虽然返回String完美地解决了修改字符串之后会导致新字符串无处存放的问题,但是如果旧的字符串(字节切片)不需要修改的话,也需要被复制到String中,这无形中增加了很多不必要的消耗;而且,字节切片中有错误字符是概率很小的事件,为了小概率事件影响拖累大概率发生的正常情况的性能,这值得吗?

这时,我一拍大腿:在需要修改时返回String,不需要修改时返回&str不就好了?

返回(Option<&str>, Option<String>)(或者Either<&str, String>

这样,上面所描述的性能和功能矛盾就解决了:

fn from_utf8_lossy<'a>(v: &'a [u8]) -> (Option<&'a str>, Option<String>) {
   todo!()
}

但这种解决方式也不是没问题的:太复杂了……而且需要用户判断返回的是&str还是String。不过,这个要么返回借用的&str、要么返回有所有权的String的东西,是不是感觉有点眼熟?

这不就是Cow<str>嘛!

最终方案:返回Cow<str>

经过一番艰难而复杂的思考,我们最终得到了最恰当的结果:

fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str> {
    todo!()
}

使用了Cow<str>之后,它不仅可以在需要修改字符串时克隆并返回新数据,更可以在绝大多数普通情况之下直接借用数据;更妙的是,它可以享受Deref转换的语法糖,可谓十分完美!

总结

Cow是Rust中非常有用的一个类型,虽然日常开发中几乎用不到它,但是某些性能敏感的场景下善用Cow说不定会有奇效喔~

posted @ 2024-02-28 16:43  Cinea  阅读(474)  评论(0编辑  收藏  举报