首页 > 其他分享 >Rust的Cow类型有什么用?详解Cow及其用途

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

时间:2024-02-28 17:00:05浏览次数:30  
标签:Cow cow 详解 let str Hello Rust String

Rust的智能指针有哪些?大多数人都能马上答出Box<T>Rc<T>Arc<T>Ref<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说不定会有奇效喔~

标签:Cow,cow,详解,let,str,Hello,Rust,String
From: https://www.cnblogs.com/cinea/p/18040997

相关文章

  • CentOS7 安装FastDFS配置详解
    一、介绍FastDFS是一个开源的高性能分布式文件系统。它的主要功能包括:文件存储,文件同步和文件访问(文件上传和文件下载),它可以解决高容量和负载平衡问题。FastDFS应该满足基于照片共享站点和视频共享站点等文件的网站的要求。FastDFS具有两个角色:tracker和storage。tracker负责调......
  • Rust的ToOwned特征:泛型版的Clone
    std::borrow::ToOwned是Rust标准库中的一个特征,用于从借用的数据中创建一个具有所有权的副本。它的作用和Clone是一样的,但是相比Clone,它支持泛型;也就是说我们可以将一个类型T“Clone”为另一个类型U。这对处理一些特殊的类型来说很有用。ToOwned的签名ToOwned提供了两个方法,其中......
  • Python面向对象,类属性,实例属性,类方法,实例方法,静态方法的区别及用法详解
    一.前言在Python的面向对象编程中,类属性和实例属性是两个不同的概念,它们在作用域和使用方式上有所区别。在Python中的面向对象编程中有三种方法:实例方法、类方法和静态方法,它们之间的差异主要体现在参数传递和调用方式上。二.面向对象-类属性和实例属性1.区别在Pyth......
  • 详解SSL证书系列(3)如何选择SSL证书
    我们知道了在网站部署SSL证书后,不管是对网站本身还是对网站的用户都能够带来许多好处。那么随着HTTPS的普及,市面上也出现了各种不同的SSL证书。并且由于SSL证书的多样性,很多人对于如何选择SSL证书有着很大的困惑。因此,本篇文章将从证书品牌,证书类型和域名类型三个方面提......
  • Lua调试函数 debug.getinfo() namewhat详解
    Lua调试的时候会用到debug.getinfo()函数,what的值文档给了解释:"Lua":Luafunction"C":Cfunction"main":mainpartofachunk(通过load函数等执行的语句)关于namewhat的值到底表示什么,官方文档只是简单列举(不全)。列举一些常见情况localgetinfo=debug.getinfol......
  • 智慧安防平台easy1400视图平台订阅功能流程详解
    在信息时代,信息的获取和传播变得至关重要。对于企业和个人而言,能够定制和接收他们感兴趣或需要的信息,将极大地提高工作效率和生活品质。本文将详细介绍如何创建和管理订阅内容,帮助您轻松掌握信息获取的主动权。 一、创建订阅首先,登录到相应的平台后,在首页界面中找到名为“下级......
  • Unity3D 光照计算方向与法线贴图详解
    在Unity3D中,光照计算方向与法线贴图是实现高质量光照效果的重要技术之一。本文将详细介绍光照计算方向与法线贴图的原理和实现方法,并给出相应的代码示例。对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白,也有一些正在从事游戏开发的技术大佬,欢迎你来交流学......
  • Unity3D 逐顶点光照与逐像素光照详解
    Unity3D提供了丰富的功能和工具,其中包括逐顶点光照和逐像素光照。本文将详细解释这两种光照技术的原理和实现方式,并提供代码示例。对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白,也有一些正在从事游戏开发的技术大佬,欢迎你来交流学习。一、逐顶点光照逐......
  • Rust的Deref特征:让智能指针“透明”的关键
    除了上篇文章中介绍过的Borrow和AsRef外,Rust中还有一个很常见的和引用相关的特征:Deref。不过,和Borrow、AsRef两个特征不同,Deref其实是用于重载解引用运算符(也就是*)的特征;在为某个类实现了Deref特征后,对它使用*运算就会调用特征中重载的方法。这篇文章不仅将介绍Deref特性,还将探......
  • this指向详解
    JavaScript中的this是一个关键字,它在不同的情况下会指向不同的值。this的取值是在函数(非箭头函数)被调用时确定的,而不是在函数被定义时确定的。1.全局上下文中:在全局上下文中,this 指向全局对象,在浏览器中通常是 window 对象。console.log(this)//window 2.函数中......