CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章 系统程序员也能享受美好事物
  • 第2章 Rust概览
  • 第3章 基本类型
  • 第4章 所有权与移动
  • 第5章 引用
  • 第6章 表达式
  • 第7章 错误处理
  • 第8章 包和模块
  • 第9章 结构体
  • 第10章 枚举和模式
  • 第11章 特性与泛型
  • 第12章 运算符重载
  • 第13章 实用特性
    • Drop
    • Sized
    • Clone
    • Copy
    • Deref 和 DerefMut
    • Default
    • AsRef 和 AsMut
    • Borrow 和 BorrowMut
    • From 和 Into
    • TryFrom 和 TryInto
    • ToOwned
    • Borrow 和 ToOwned 的应用:实用的 Cow
  • 第14章 闭包
  • 第15章 迭代器
  • 第16章 集合
  • 第17章 字符串和文本
  • 第18章 输入与输出
  • 第19章 并发
  • 第20章 异步编程
  • Rust编程指南
zhangxf
2025-03-10
目录

第13章 实用特性

# 第13章 实用特性

科学只不过是在大自然的千变万化中——或者更确切地说,在我们千变万化的经验中——寻找统一性的探索。用柯勒律治(Coleridge)的话说,诗歌、绘画和艺术也是同样的探索,即在变化中寻求统一。 ——雅各布·布罗诺夫斯基(Jacob Bronowski)

本章将介绍我们所说的Rust “实用” 特性,这些特性来自标准库,种类繁多,它们对Rust的编程方式有着重要影响。为了编写符合习惯用法的代码,以及为你的库设计出用户认为 “很Rust” 的公共接口,你需要熟悉这些特性。它们大致可分为三类:

  • 语言扩展特性:正如我们在上一章介绍的运算符重载特性,让你能够在自己定义的类型上使用Rust的表达式运算符一样,标准库中还有其他一些特性,作为Rust的扩展点,能让你将自己的类型与语言更紧密地集成。这些特性包括Drop、Deref和DerefMut,以及转换特性From和Into。我们将在本章介绍这些特性。
  • 标记特性:这些特性主要用于约束泛型类型变量,以表达其他方式无法表达的约束。包括Sized和Copy。
  • 公共词汇特性:这些特性没有任何神奇的编译器集成功能;你也可以在自己的代码中定义等效的特性。但它们有一个重要作用,就是为常见问题提供了约定俗成的解决方案。这些特性在库与模块之间的公共接口中特别有价值:通过减少不必要的差异,它们使接口更易于理解,同时也增加了不同库的功能可以直接组合使用的可能性,而无需编写样板代码或自定义粘合代码。这些特性包括Default、引用借用特性AsRef、AsMut、Borrow和BorrowMut;可能失败的转换特性TryFrom和TryInto;以及ToOwned特性,它是Clone的泛化。

这些特性总结在表13-1中。 表13-1 实用特性总结

特性 描述
Drop 析构函数。Rust在值被丢弃时自动运行的清理代码。
Sized 标记特性,用于在编译时已知大小固定的类型,与动态大小的类型(如切片)相对。
Clone 支持克隆值的类型。
Copy 标记特性,用于可以通过对包含值的内存进行逐字节复制来实现克隆的类型。
Deref和DerefMut 智能指针类型的特性。
Default 具有合理 “默认值” 的类型。
AsRef和AsMut 从一种类型借用另一种类型引用的转换特性。
Borrow和BorrowMut 与AsRef/AsMut类似的转换特性,但额外保证一致的哈希、排序和相等性。
From和Into 将一种类型的值转换为另一种类型的转换特性。
TryFrom和TryInto 用于可能失败的类型转换的转换特性。
ToOwned 将引用转换为拥有所有权的值的转换特性。

标准库中还有其他重要的特性。我们将在第15章介绍Iterator和IntoIterator。第16章会介绍用于计算哈希码的Hash特性。第19章将介绍标记线程安全类型的一对特性Send和Sync。

# Drop

当一个值的所有者消失时,我们说Rust丢弃了这个值。丢弃一个值意味着释放该值所拥有的其他任何值、堆存储和系统资源。丢弃值的情况有很多:变量超出作用域时;在表达式语句结束时;当你截断向量,从其末尾移除元素时等等。

在大多数情况下,Rust会自动为你处理值的丢弃。例如,假设你定义了以下类型:

struct Appellation {
    name: String,
    nicknames: Vec<String>
}
1
2
3
4

一个Appellation拥有用于存储字符串内容的堆空间和向量元素的缓冲区。每当一个Appellation被丢弃时,Rust会负责清理所有这些资源,你无需再编写额外的代码。不过,如果你愿意,可以通过实现std::ops::Drop特性来自定义Rust丢弃你定义类型的值的方式:

trait Drop {
    fn drop(&mut self);
}
1
2
3

Drop的实现类似于C++中的析构函数,或者其他语言中的终结器。当一个值被丢弃时,如果它实现了std::ops::Drop,Rust会在像往常一样继续丢弃其字段或元素所拥有的值之前,调用其drop方法。这种对drop的隐式调用是调用该方法的唯一方式;如果你试图显式调用它,Rust会将其标记为错误。

因为Rust在丢弃一个值的字段或元素之前会调用Drop::drop,所以该方法接收到的值始终是完全初始化的。我们为Appellation类型实现的Drop可以充分利用其字段:

impl Drop for Appellation {
    fn drop(&mut self) {
        print!("Dropping {}", self.name);
        if !self.nicknames.is_empty() {
            print!(" (AKA {})", self.nicknames.join(", "));
        }
        println!("");
    }
}
1
2
3
4
5
6
7
8
9

有了这个实现,我们可以编写以下代码:

{
    let mut a = Appellation {
        name: "Zeus".to_string(),
        nicknames: vec!["cloud collector".to_string(), "king of the gods".to_string()]
    };
    println!("before assignment");
    a = Appellation { name: "Hera".to_string(), nicknames: vec![] };
    println!("at end of block");
}
1
2
3
4
5
6
7
8
9

当我们将第二个Appellation赋值给a时,第一个会被丢弃;当a超出作用域时,第二个也会被丢弃。这段代码的输出如下:

before assignment
Dropping Zeus (AKA cloud collector, king of the gods)
at end of block
Dropping Hera
1
2
3
4

由于我们为Appellation实现的std::ops::Drop只是打印一条消息,那么它的内存到底是如何清理的呢?Vec类型实现了Drop,它会丢弃每个元素,然后释放它们占用的堆分配缓冲区。String内部使用Vec<u8>来存储文本,所以String本身无需实现Drop;它让其内部的Vec来负责释放字符。同样的原理也适用于Appellation值:当一个Appellation值被丢弃时,最终是Vec的Drop实现负责释放每个字符串的内容,最后释放存储向量元素的缓冲区。至于存储Appellation值本身的内存,它也有某个所有者,可能是一个局部变量或某个数据结构,由这个所有者负责释放它。

如果一个变量的值被移动到其他地方,导致该变量在超出作用域时未初始化,那么Rust不会尝试丢弃该变量:因为其中没有值可丢弃。

即使一个变量的值是否被移动取决于控制流,这条原则仍然适用。在这种情况下,Rust会用一个不可见的标志来跟踪变量的状态,指示该变量的值是否需要被丢弃:

let p;
{
    let q = Appellation { name: "Cardamine hirsuta".to_string(),
        nicknames: vec!["shotweed".to_string(), "bittercress".to_string()] };
    if complicated_condition() {
        p = q;
    }
}
println!("Sproing! What was that?");
1
2
3
4
5
6
7
8
9

根据complicated_condition返回true还是false,p或q最终会拥有Appellation,而另一个则未初始化。Appellation值的最终归属决定了它是在println!之前还是之后被丢弃,因为q在println!之前超出作用域,而p在之后。虽然一个值可能会在不同地方移动,但Rust只会丢弃它一次。

通常,除非你定义的类型拥有Rust尚未知晓的资源,否则不需要实现std::ops::Drop。例如,在Unix系统上,Rust标准库内部使用以下类型来表示操作系统文件描述符:

struct FileDesc {
    fd: c_int,
}
1
2
3

FileDesc的fd字段只是程序使用完后应该关闭的文件描述符编号;c_int是i32的别名。标准库为FileDesc实现Drop如下:

impl Drop for FileDesc {
    fn drop(&mut self) {
        let _ = unsafe { libc::close(self.fd) };
    }
}
1
2
3
4
5

这里,libc::close是Rust对C库close函数的称呼。Rust代码只能在unsafe块中调用C函数,所以这里使用了unsafe块。

如果一个类型实现了Drop,它就不能再实现Copy特性。如果一个类型是Copy,这意味着简单的逐字节复制就足以生成该值的独立副本。但通常情况下,对同一数据多次调用相同的drop方法是错误的。

标准前置模块中包含一个用于丢弃值的函数drop,但它的定义并没有什么神奇之处:

fn drop<T>(_x: T) {}
1

换句话说,它按值接收参数,从调用者那里获取所有权,然后不对其进行任何操作。当_x超出作用域时,Rust会像处理其他任何变量一样丢弃它的值。

# Sized

大小确定(sized)类型指的是其所有值在内存中大小都相同的类型。Rust中几乎所有类型都是大小确定的:每个u64占用8个字节,每个(f32, f32, f32)元组占用12个字节。甚至枚举类型也是大小确定的:无论实际使用哪个变体,枚举始终占用足够的空间来容纳其最大的变体。虽然Vec<T>拥有一个大小可变的堆分配缓冲区,但Vec值本身是一个指向缓冲区、容量和长度的指针,所以Vec<T>是一个大小确定的类型。

所有大小确定的类型都实现了std::marker::Sized特性,该特性没有方法或关联类型。Rust会自动为所有适用的类型实现它,你不能自己实现这个特性。Sized唯一的用途是作为类型变量的约束:像T: Sized这样的约束要求T是一个在编译时已知大小的类型。这类特性被称为标记特性(marker traits),因为Rust语言本身用它们来标记某些具有特定有趣特征的类型。

不过,Rust也有一些大小不确定(unsized)的类型,其值的大小并不都相同。例如,字符串切片类型str(注意,这里没有&)是大小不确定的。字符串字面量"diminutive"和"big"是指向str切片的引用,分别占用10个字节和3个字节。如图13-1所示。像[T](同样没有&)这样的数组切片类型也是大小不确定的:像&[u8]这样的共享引用可以指向任意大小的[u8]切片。由于str和[T]类型表示大小不同的值的集合,所以它们是大小不确定的类型。 img 图13-1 指向大小不确定值的引用

Rust中另一种常见的大小不确定的类型是dyn类型,即特性对象的引用目标。正如我们在 “特性对象” 中解释的,特性对象是指向某个实现了给定特性的值的指针。例如,&dyn std::io::Write和Box<dyn std::io::Write>类型是指向某个实现了Write特性的值的指针。该引用目标可能是一个文件、网络套接字,或者是你自己实现了Write特性的某种类型。由于实现Write特性的类型集合是开放的,所以dyn Write作为一种类型是大小不确定的:它的值有不同的大小。

Rust不能将大小不确定的值存储在变量中,也不能将它们作为参数传递。你只能通过像&str或Box<dyn Write>这样的指针来处理它们,而这些指针本身是大小确定的。如图13-1所示,指向大小不确定值的指针总是一个胖指针,占用两个机器字长:指向切片的指针还会携带切片的长度,特性对象还会携带一个指向方法实现虚表(vtable)的指针。

特性对象和指向切片的指针有很好的对称性。在这两种情况下,类型都缺少使用它所需的信息:不知道[u8]的长度,你就无法对其进行索引;不知道Box<dyn Write>所指向的具体值对应的Write实现,你就无法调用其方法。在这两种情况下,胖指针都补充了类型所缺少的信息,携带了长度或虚表指针。省略的静态信息被动态信息所取代。

由于大小不确定的类型有诸多限制,大多数泛型类型变量应该限制为大小确定的类型。实际上,这种需求非常常见,以至于在Rust中这是隐式默认的:如果你写struct S<T> { ... },Rust会理解为struct S<T: Sized> { ... }。如果你不想这样约束T,就必须显式取消这种限制,写成struct S<T: ?Sized> { ... }。?Sized语法仅用于这种情况,意思是 “不一定是大小确定的”。例如,如果你写struct S<T: ?Sized> { b: Box<T> },那么Rust会允许你使用S<str>和S<dyn Write>,此时Box会变成胖指针;同时也允许使用S<i32>和S<String>,此时Box是普通指针。

尽管有这些限制,大小不确定的类型使Rust的类型系统工作得更加顺畅。阅读标准库文档时,你偶尔会遇到类型变量上的?Sized约束;这几乎总是意味着给定的类型仅通过指针使用,并且允许相关代码同时处理切片、特性对象和普通值。当一个类型变量有?Sized约束时,人们通常说它的大小是不确定的:它可能是大小确定的,也可能不是。

除了切片和特性对象,还有一种大小不确定的类型。结构体类型的最后一个字段(但只能是最后一个字段)可以是大小不确定的,这样的结构体本身也是大小不确定的。例如,Rc<T>引用计数指针在内部被实现为指向私有类型RcBox<T>的指针,RcBox<T>将引用计数与T存储在一起。下面是RcBox的简化定义:

struct RcBox<T: ?Sized> {
    ref_count: usize,
    value: T,
}
1
2
3
4

value字段是Rc<T>正在计数引用的T;Rc<T>解引用后得到的指针指向这个字段。ref_count字段保存引用计数。

真正的RcBox只是标准库的实现细节,不供公开使用。但假设我们使用前面的定义。你可以将这个RcBox与大小确定的类型一起使用,比如RcBox<String>,结果是一个大小确定的结构体类型。你也可以将它与大小不确定的类型一起使用,比如RcBox<dyn std::fmt::Display>(其中Display是用于可以被println!和类似宏格式化的类型的特性);RcBox<dyn Display>是一个大小不确定的结构体类型。

你不能直接构建一个RcBox<dyn Display>值。相反,你首先需要创建一个普通的、大小确定的RcBox,其值类型要实现Display,比如RcBox<String>。然后,Rust允许你将引用&RcBox<String>转换为胖引用&RcBox<dyn Display>:

let boxed_lunch: RcBox<String> = RcBox { ref_count: 1, value: "lunch".to_string() };
use std::fmt::Display;
let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;
1
2
3

在将值传递给函数时,这种转换会隐式发生,所以你可以将&RcBox<String>传递给一个期望&RcBox<dyn Display>的函数:

fn display(boxed: &RcBox<dyn Display>) {
    println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
1
2
3
4

这将产生以下输出:For your enjoyment: lunch

# Clone

std::clone::Clone特性用于那些可以创建自身副本的类型。Clone定义如下:

trait Clone : Sized {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}
1
2
3
4
5
6

clone方法应该构造一个self的独立副本并返回。由于这个方法的返回类型是Self,并且函数不能返回大小不确定的值,所以Clone特性本身扩展了Sized特性:这使得实现Clone特性的Self类型必须是大小确定的。

克隆一个值通常还需要分配其拥有的任何内容的副本,所以克隆在时间和内存上可能开销很大。例如,克隆一个Vec<String>不仅要复制向量,还要复制其中的每个String元素。这就是为什么Rust不会自动克隆值,而是要求你显式调用方法。像Rc<T>和Arc<T>这样的引用计数指针类型是例外:克隆它们只是增加引用计数并返回一个新指针。

clone_from方法将self修改为source的副本。clone_from的默认定义只是克隆source,然后将其移动到*self中。这总是可行的,但对于某些类型,有更快的方法可以达到相同的效果。例如,假设s和t是String类型。语句s = t.clone();必须克隆t,丢弃s的旧值,然后将克隆的值移动到s中;这涉及一次堆分配和一次堆释放。但是,如果原来s的堆缓冲区有足够的容量来容纳t的内容,就不需要进行分配或释放操作:你可以直接将t的文本复制到s的缓冲区中并调整长度。在泛型代码中,只要可能,你都应该使用clone_from,以便在有优化实现时利用它们。

如果你的Clone实现只是对类型的每个字段或元素应用clone,然后用这些克隆值构造一个新值,并且clone_from的默认定义就足够了,那么Rust会为你实现这些:只需在类型定义上方加上#[derive(Clone)]。

标准库中几乎所有适合复制的类型都实现了Clone。像bool和i32这样的基本类型实现了该特性。像String、Vec<T>和HashMap这样的容器类型也实现了。有些类型不适合复制,比如std::sync::Mutex,这些类型没有实现Clone。有些类型,比如std::fs::File,可以复制,但如果操作系统没有必要的资源,复制可能会失败;这些类型没有实现Clone,因为clone必须是可靠的。相反,std::fs::File提供了一个try_clone方法,它返回一个std::io::Result<File>,可以报告复制失败的情况。

# Copy

在第4章中,我们解释过,对于大多数类型而言,赋值操作是移动值,而非复制值。移动值能让跟踪它们所拥有的资源变得更为简单。但在 “复制类型:移动的例外情况” 中,我们指出了例外情况:不拥有任何资源的简单类型可以是复制(Copy)类型,在这类类型中,赋值会对源值进行复制,而不是移动值并使源值处于未初始化状态。

当时,我们并未明确说明复制(Copy)究竟是什么,现在可以告诉你:如果一个类型实现了std::marker::Copy标记特性,那么它就是复制类型,该特性定义如下:

trait Copy : Clone { }
1

为你自己定义的类型实现这个特性确实很容易:

impl Copy for MyType { }
1

但由于Copy是一个对语言有着特殊意义的标记特性,Rust仅允许在只需进行浅字节逐字节复制的情况下,一个类型才能实现Copy。拥有诸如堆缓冲区或操作系统句柄等其他任何资源的类型,都无法实现Copy。

任何实现了Drop特性的类型都不可能是Copy类型。Rust认为,如果一个类型需要特殊的清理代码,那它必然也需要特殊的复制代码,因此不能成为Copy类型。

与Clone特性一样,你可以让Rust为你派生Copy特性,使用#[derive(Copy)]。你经常会看到同时派生这两个特性,即#[derive(Copy, Clone)]。

在将一个类型设为Copy类型之前,请仔细考虑。尽管这样做会让类型使用起来更方便,但它对类型的实现有着严格的限制。隐式复制也可能会带来较高的开销。我们在 “复制类型:移动的例外情况” 中详细解释了这些因素。

# Deref 和 DerefMut

你可以通过实现std::ops::Deref和std::ops::DerefMut特性,来指定像*和.这样的解引用运算符在你的类型上的行为方式。诸如Box<T>和Rc<T>等指针类型实现了这些特性,以便它们能像Rust的内置指针类型那样工作。例如,如果你有一个Box<Complex>值b,那么*b指的是b所指向的Complex值,b.re指的是其真实分量。如果上下文对引用目标进行赋值或借用可变引用,Rust会使用DerefMut(“可变解引用” )特性;否则,只读访问就足够了,此时会使用Deref特性。

这些特性的定义如下:

trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

trait DerefMut : Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}
1
2
3
4
5
6
7
8

deref和deref_mut方法接受一个&Self引用,并返回一个&Self::Target引用。Target应该是Self包含、拥有或引用的内容:对于Box<Complex>,Target类型是Complex。请注意,DerefMut扩展了Deref:如果你能对某个值进行解引用并修改它,那么你肯定也应该能够借用对它的共享引用。由于这些方法返回的引用与&self具有相同的生命周期,所以只要返回的引用存在,self就会一直处于被借用状态。

Deref和DerefMut特性还有另一个作用。由于deref接受一个&Self引用并返回一个&Self::Target引用,Rust利用这一点自动将前一种类型的引用转换为后一种类型。换句话说,如果插入一个解引用调用能够避免类型不匹配,Rust会为你插入一个。实现DerefMut则为可变引用启用了相应的转换。这些被称为解引用强制转换(deref coercions):一种类型被 “强制” 表现得像另一种类型。

虽然解引用强制转换的操作你自己也可以显式地写出来,但它们非常方便:

  • 如果你有某个Rc<String>值r,并想对它应用String::find方法,你可以直接写r.find('?'),而不是(*r).find('?'):方法调用会隐式借用r,并且由于Rc<T>实现了Deref<Target=T>,&Rc<String>会强制转换为&String。
  • 你可以对String值使用诸如split_at这样的方法,尽管split_at是str切片类型的方法,这是因为String实现了Deref<Target=str>。String无需重新实现str的所有方法,因为你可以从&String强制转换得到&str。
  • 如果你有一个字节向量v,并且想将它传递给一个期望字节切片&[u8]的函数,你可以直接将&v作为参数传递,因为Vec<T>实现了Deref<Target=[T]>。

如有必要,Rust会连续应用多个解引用强制转换。例如,利用前面提到的强制转换,你可以直接对Rc<String>应用split_at方法,因为&Rc<String>解引用后得到&String,&String再解引用得到&str,而&str有split_at方法。例如,假设你有以下类型:

struct Selector<T> {
    /// 此`Selector`中可用的元素。
    elements: Vec<T>,
    /// `elements`中“当前”元素的索引。`Selector`的行为类似于指向当前元素的指针。
    current: usize
}
1
2
3
4
5
6

为了让Selector的行为符合文档注释中的描述,你必须为该类型实现Deref和DerefMut:

use std::ops::{Deref, DerefMut};

impl<T> Deref for Selector<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.elements[self.current]
    }
}

impl<T> DerefMut for Selector<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.elements[self.current]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

有了这些实现,你可以像这样使用Selector:

let mut s = Selector { elements: vec!['x', 'y', 'z'], current: 2 };
// 因为`Selector`实现了`Deref`,我们可以使用`*`运算符来引用其当前元素。
assert_eq!(*s, 'z');
// 通过解引用强制转换,直接在`Selector`上使用`char`的方法,断言`'z'`是字母。
assert!(s.is_alphabetic());
// 通过对`Selector`的引用目标赋值,将`'z'`改为`'w'`。
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);
1
2
3
4
5
6
7
8

Deref和DerefMut特性是为实现智能指针类型(如Box、Rc和Arc),以及那些作为你经常通过引用使用的类型的拥有所有权版本(就像Vec<T>和String分别是[T]和str的拥有所有权版本)而设计的。你不应仅仅为了让Target类型的方法自动出现在你的类型上,就像C++中基类的方法在子类中可见那样,而为一个类型实现Deref和DerefMut。这并不总是能如你所愿地工作,而且一旦出现问题会令人困惑。

解引用强制转换有一个可能会让人感到困惑的注意事项:Rust应用它们来解决类型冲突,但不会用它们来满足类型变量的约束。例如,以下代码运行正常:

let s = Selector { elements: vec!["good", "bad", "ugly"], current: 2 };
fn show_it(thing: &str) { println!("{}", thing); }
show_it(&s);
1
2
3

在show_it(&s)这个调用中,Rust看到参数类型为&Selector<&str>,而函数参数类型为&str,它会找到Deref<Target=str>的实现,并根据需要将调用改写为show_it(s.deref())。

然而,如果你将show_it改为一个泛型函数,Rust就突然不配合了:

use std::fmt::Display;
fn show_it_generic<T: Display>(thing: T) { println!("{}", thing); }
show_it_generic(&s);
1
2
3

Rust会报错:

error: `Selector<&str>` doesn't implement `std::fmt::Display`
|
33 |     fn show_it_generic<T: Display>(thing: T) { println!("
{}", thing); }
|       ------- required by this bound in `show_it_generic`
34 |     show_it_generic(&s);
|     ^^ `Selector<&str>` cannot be formatted with the
|     &*s
1
2
3
4
5
6
7
8

这可能会让人感到困惑:为什么将函数泛型化会引入错误呢?确实,Selector<&str>本身没有实现Display,但它解引用后得到的&str肯定实现了。

由于你传递的参数类型是&Selector<&str>,而函数的参数类型是&T,所以类型变量T必须是Selector<&str>。然后,Rust会检查T: Display这个约束是否满足:由于它不会应用解引用强制转换来满足类型变量的约束,所以这个检查失败了。

为了解决这个问题,你可以使用as运算符明确写出强制转换:

show_it_generic(&s as &str);
1

或者,按照编译器的建议,使用&*来强制进行转换:

show_it_generic(&*s);
1

# Default

有些类型具有相当明显的默认值:默认的向量或字符串为空,默认的数字是零,默认的Option是None等等。这类类型可以实现std::default::Default特性:

trait Default {
    fn default() -> Self;
}
1
2
3

default方法只是返回一个新的Self类型的值。String对Default的实现很直观:

impl Default for String {
    fn default() -> String {
        String::new()
    }
}
1
2
3
4
5

Rust的所有集合类型,如Vec、HashMap、BinaryHeap等等,都实现了Default,其default方法返回一个空集合。当你需要构建一个值的集合,但又想让调用者决定具体构建哪种集合时,这会很有帮助。例如,Iterator特性的partition方法会使用一个闭包来决定迭代器生成的值的去向,将这些值分割到两个集合中:

use std::collections::HashSet;

let squares = [4, 9, 16, 25, 36, 49, 64];
let (powers_of_two, impure): (HashSet<i32>, HashSet<i32>) = squares.iter().partition(|&n| n & (n - 1) == 0);
assert_eq!(powers_of_two.len(), 3);
assert_eq!(impure.len(), 4);
1
2
3
4
5
6

闭包|&n| n & (n - 1) == 0通过位运算来识别2的幂次方数,partition方法利用这个闭包生成了两个HashSet。当然,partition并不局限于HashSet;只要集合类型实现了Default(用于生成一个空集合作为起始)和Extend<T>(用于向集合中添加一个T类型的值),你就可以用它生成任何你想要的集合。String实现了Default和Extend<char>,所以你可以这样写:

let (upper, lower): (String, String) = "Great Teacher Onizuka".chars().partition(|&c| c.is_uppercase());
assert_eq!(upper, "GTO");
assert_eq!(lower, "reat eacher nizuka");
1
2
3

Default的另一个常见用途是为表示大量参数的结构体生成默认值,其中大多数参数你通常不需要更改。例如,glium库为强大而复杂的OpenGL图形库提供了Rust绑定。glium::DrawParameters结构体包含24个字段,每个字段控制OpenGL渲染图形的不同细节。glium的draw函数期望一个DrawParameters结构体作为参数。由于DrawParameters实现了Default,你可以创建一个并传递给draw函数,只需要指定你想要更改的字段:

let params = glium::DrawParameters {
    line_width: Some(0.02),
    point_size: Some(0.02),
   ..Default::default()
};
target.draw(..., &params).unwrap();
1
2
3
4
5
6

这会调用Default::default()来创建一个DrawParameters值,其所有字段都初始化为默认值,然后使用结构体的..语法创建一个新的DrawParameters值,只更改line_width和point_size字段,准备好将其传递给target.draw。

如果一个类型T实现了Default,那么标准库会自动为Rc<T>、Arc<T>、Box<T>、Cell<T>、RefCell<T>、Cow<T>、Mutex<T>和RwLock<T>实现Default。例如,Rc<T>的默认值是一个指向T类型默认值的Rc。

如果一个元组类型的所有元素类型都实现了Default,那么该元组类型也实现Default,其默认值是一个包含每个元素默认值的元组。

Rust不会为结构体类型隐式实现Default,但如果一个结构体的所有字段都实现了Default,你可以使用#[derive(Default)]自动为该结构体实现Default。

# AsRef 和 AsMut

当一个类型实现了AsRef<T>,这意味着你可以高效地从它那里借用一个&T。AsMut是对应可变引用的类似特性。它们的定义如下:

trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}
1
2
3
4
5
6
7

例如,Vec<T>实现了AsRef<[T]>,String实现了AsRef<str>。你还可以将String的内容作为字节数组借用,所以String也实现了AsRef<[u8]>。

AsRef通常用于使函数在接受参数类型时更加灵活。例如,std::fs::File::open函数的声明如下:

fn open<P: AsRef<Path>>(path: P) -> Result<File>
1

open真正需要的是一个&Path,这是表示文件系统路径的类型。但有了这个签名,open可以接受任何它能借用&Path的类型,即任何实现了AsRef<Path>的类型。这类类型包括String、str、操作系统接口字符串类型OsString和OsStr,当然还有PathBuf和Path;完整列表请查看库文档。这就是为什么你可以将字符串字面量传递给open函数:

let dot_emacs = std::fs::File::open("/home/jimb/.emacs")?;
1

标准库中所有的文件系统访问函数都以这种方式接受路径参数。对于调用者来说,其效果类似于C++中的重载函数,尽管Rust在确定哪些参数类型可接受方面采用了不同的方法。

但事情并非这么简单。字符串字面量是&str,而实现AsRef<Path>的类型是str,没有&。正如我们在 “Deref和DerefMut” 中解释的,Rust不会尝试使用解引用强制转换来满足类型变量的约束,所以在这里它们也帮不上忙。

幸运的是,标准库包含了一个通用实现:

impl<'a, T, U> AsRef<U> for &'a T
where
    T: AsRef<U>,
    T: ?Sized,
    U: ?Sized
{
    fn as_ref(&self) -> &U {
        (*self).as_ref()
    }
}
1
2
3
4
5
6
7
8
9
10

换句话说,对于任何类型T和U,如果T: AsRef<U>,那么&T: AsRef<U>也成立:只需沿着引用继续操作即可。特别地,由于str: AsRef<Path>,所以&str: AsRef<Path>也成立。从某种意义上说,这是一种在检查AsRef类型变量约束时获得有限形式解引用强制转换的方法。

你可能会认为,如果一个类型实现了AsRef<T>,它也应该实现AsMut<T>。然而,在某些情况下这并不合适。例如,我们提到过String实现了AsRef<[u8]>,这是有意义的,因为每个String都有一个字节缓冲区,可以作为二进制数据来访问。但是,String进一步保证了这些字节是格式良好的Unicode文本的UTF-8编码;如果String实现了AsMut<[u8]>,调用者就可以随意更改String的字节,这样你就不能再保证String是格式良好的UTF-8了。只有当修改给定的T不会违反类型的不变性时,一个类型实现AsMut<T>才有意义。

虽然AsRef和AsMut相当简单,但提供标准的、通用的引用转换特性可以避免更具体的转换特性的泛滥。如果可以实现AsRef<Foo>,你就应该避免定义自己的AsFoo特性。

# Borrow 和 BorrowMut

std::borrow::Borrow特性与AsRef类似:如果一个类型实现了Borrow<T>,那么它的borrow方法可以高效地从它那里借用一个&T。但Borrow施加了更多限制:一个类型只有在&T的哈希值和比较方式与被借用的值相同时,才应该实现Borrow<T>。(Rust不会强制这一点,这只是该特性文档中说明的意图。)这使得Borrow在处理哈希表和树中的键,或者处理因其他原因需要进行哈希或比较的值时很有价值。

例如,从String借用时,这种区别就很重要:String实现了AsRef<str>、AsRef<[u8]>和AsRef<Path>,但这三种目标类型通常具有不同的哈希值。只有&str切片保证与等效的String具有相同的哈希值,所以String只实现了Borrow<str>。

Borrow的定义与AsRef相同,只是名称不同:

trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}
1
2
3

Borrow是为解决泛型哈希表和其他关联集合类型的特定情况而设计的。例如,假设你有一个std::collections::HashMap<String, i32>,用于将字符串映射到数字。这个表的键是String类型,每个条目都拥有一个String。在这个表中查找条目的方法签名应该是什么样的呢?下面是第一次尝试:

impl<K, V> HashMap<K, V>
where
    K: Eq + Hash
{
    fn get(&self, key: K) -> Option<&V> { ... }
}
1
2
3
4
5
6

这看起来很合理:要查找一个条目,你必须提供适合该表的键类型。但在这种情况下,K是String;这个签名会强制你每次调用get时都按值传递一个String,这显然很浪费。实际上,你只需要一个指向键的引用:

impl<K, V> HashMap<K, V>
where
    K: Eq + Hash
{
    fn get(&self, key: &K) -> Option<&V> { ... }
}
1
2
3
4
5
6

这稍微好一些,但现在你必须将键作为&String传递,所以如果你想查找一个常量字符串,就必须这样写:

hashtable.get(&"twenty-two".to_string())
1

这很荒谬:它在堆上分配一个String缓冲区,将文本复制到其中,只是为了将其作为&String借用,传递给get,然后再丢弃它。

传递任何可以与我们的键类型进行哈希和比较的东西就足够了,例如&str就完全可以。所以最终的版本是这样的,这也是你在标准库中会看到的:

impl<K, V> HashMap<K, V>
where
    K: Eq + Hash
{
    fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Eq + Hash
    { ... }
}
1
2
3
4
5
6
7
8
9
10

换句话说,如果你可以将条目的键借用为&Q,并且得到的引用的哈希值和比较方式与键本身相同,那么显然&Q应该是可接受的键类型。由于String实现了Borrow<str>和Borrow<String>,这个最终版本的get方法允许你根据需要传递&String或&str作为键。

Vec<T>和[T; N]实现了Borrow<[T]>。每个类似字符串的类型都允许借用其相应的切片类型:String实现了Borrow<str>,PathBuf实现了Borrow<Path>等等。标准库中所有的关联集合类型都使用Borrow来决定哪些类型可以传递给它们的查找函数。

标准库包含一个通用实现,使得每个类型T都可以从自身借用:T: Borrow<T>。这确保了&K始终是在HashMap<K, V>中查找条目的可接受类型。

为了方便起见,每个&mut T类型也实现了Borrow<T>,像往常一样返回一个共享引用&T。这允许你将可变引用传递给集合查找函数,而无需重新借用一个共享引用,模拟了Rust通常从可变引用到共享引用的隐式强制转换。

BorrowMut特性是Borrow对应可变引用的版本:

trait BorrowMut<Borrowed: ?Sized> : Borrow<Borrowed> {
    fn borrow_mut(&mut self) -> &mut Borrowed;
}
1
2
3

对Borrow的期望同样适用于BorrowMut。

# From 和 Into

std::convert::From和std::convert::Into特性表示将一种类型的值消耗并返回另一种类型的值的转换。AsRef和AsMut特性是从一种类型借用另一种类型的引用,而From和Into则获取其参数的所有权,对其进行转换,然后将结果的所有权返回给调用者。

它们的定义非常对称:

trait Into<T> : Sized {
    fn into(self) -> T;
}

trait From<T> : Sized {
    fn from(other: T) -> Self;
}
1
2
3
4
5
6
7

标准库会自动为每种类型实现从自身到自身的平凡转换:每个类型T都实现了From<T>和Into<T>。

虽然这两个特性只是提供了两种做同一件事的方式,但它们有不同的用途。

你通常使用Into来使你的函数在接受参数时更加灵活。例如,如果你这样写:

use std::net::Ipv4Addr;

fn ping<A>(address: A) -> std::io::Result<bool>
where
    A: Into<Ipv4Addr>
{
    let ipv4_address = address.into();
   ...
}
1
2
3
4
5
6
7
8
9

那么ping函数不仅可以接受Ipv4Addr作为参数,还可以接受u32或[u8; 4]数组,因为这些类型恰好都实现了Into<Ipv4Addr>。(有时将IPv4地址当作一个32位的值或一个4字节的数组很有用。)因为ping函数只知道address实现了Into<Ipv4Addr>,所以在调用into时,你不需要指定想要的具体类型;只有一种类型可能有效,所以类型推断会为你补充。

与上一节的AsRef类似,其效果很像C++中的函数重载。有了前面定义的ping函数,我们可以进行以下任何调用:

println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // 传递一个Ipv4Addr
println!("{:?}", ping([66, 146, 219, 98])); // 传递一个[u8; 4]
println!("{:?}", ping(0xd076eb94_u32)); // 传递一个u32
1
2
3

然而,From特性起着不同的作用。from方法作为一个通用构造函数,用于从某个其他单一值生成一个类型的实例。例如,Ipv4Addr没有名为from_array和from_u32的两个方法,而是简单地实现了From<[u8;4]>和From<u32>,这样我们就可以这样写:

let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
let addr2 = Ipv4Addr::from(0xd076eb94_u32);
1
2

我们可以让类型推断来确定应用哪个实现。

有了合适的From实现,标准库会自动实现相应的Into特性。当你定义自己的类型时,如果它有单参数构造函数,你应该将它们写成针对适当类型的From<T>实现;这样你会自动获得相应的Into实现。

因为from和into转换方法获取其参数的所有权,所以转换可以重用原始值的资源来构造转换后的值。例如,假设你这样写:

let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
1
2

String对Into<Vec<u8>>的实现只是获取String的堆缓冲区,并将其原封不动地重新用作返回向量的元素缓冲区。这种转换无需分配或复制文本。这是移动操作实现高效转换的另一个例子。

这些转换还提供了一种很好的方式,在不削弱受限类型保证的情况下,将受限类型的值转换为更灵活的类型。例如,String保证其内容始终是有效的UTF-8;其可变方法受到严格限制,以确保你做的任何操作都不会引入错误的UTF-8。但在这个例子中,它有效地将String“降级”为一块普通字节,你可以对其进行任何操作:也许你要对它进行压缩,或者将它与其他不是UTF-8的二进制数据组合。因为into按值获取其参数,转换后text不再初始化,这意味着我们可以自由访问之前String的缓冲区,而不会破坏任何现有的String。

然而,廉价转换并不是Into和From的约定内容。虽然AsRef和AsMut转换预期是廉价的,但From和Into转换可能会分配内存、复制数据或以其他方式处理值的内容。例如,String实现了From<&str>,它会将字符串切片复制到为String新分配的堆缓冲区中。std::collections::BinaryHeap<T>实现了From<Vec<T>>,它会根据其算法要求对元素进行比较和重新排序。

?运算符使用From和Into来帮助清理可能以多种方式失败的函数中的代码,在需要时自动将特定的错误类型转换为通用错误类型。

例如,假设有一个系统需要读取二进制数据,并将其中一部分以UTF-8文本形式表示的十进制数字进行转换。这意味着要使用std::str::from_utf8和i32的FromStr实现,它们各自可能返回不同类型的错误。假设我们使用在第7章讨论错误处理时定义的GenericError和GenericResult类型,?运算符会为我们进行转换:

type GenericError = Box<dyn std::error::Error + Send + Sync +'static>;
type GenericResult<T> = Result<T, GenericError>;

fn parse_i32_bytes(b: &[u8]) -> GenericResult<i32> {
    Ok(std::str::from_utf8(b)?.parse::<i32>()?)
}
1
2
3
4
5
6

与大多数错误类型一样,Utf8Error和ParseIntError实现了Error特性,标准库为我们提供了一个通用的From实现,用于将任何实现了Error的类型转换为Box<dyn Error>,?运算符会自动使用这个实现:

impl<'a, E: Error + Send + Sync + 'a> From<E>
for Box<dyn Error + Send + Sync + 'a> {
    fn from(err: E) -> Box<dyn Error + Send + Sync + 'a> {
        Box::new(err)
    }
}
1
2
3
4
5
6

这将原本可能需要两个match语句的相当长的函数变成了一行代码。

在From和Into被添加到标准库之前,Rust代码中充满了特定于单个类型的临时转换特性和构造方法。From和Into将这些约定规范化,你可以遵循这些约定,使你的类型更易于使用,因为你的用户已经熟悉它们了。其他库和语言本身也可以依赖这些特性,将其作为一种规范的、标准化的转换编码方式。

From和Into是不可失败的特性——它们的API要求转换不会失败。不幸的是,许多转换比这更复杂。例如,像i64这样的大整数可以存储比i32大得多的数字,如果没有一些额外信息,将像2_000_000_000_000i64这样的数字转换为i32就没有多大意义。进行简单的位运算转换,即丢弃前32位,通常不会得到我们期望的结果:

let huge = 2_000_000_000_000i64;
let smaller = huge as i32;
println!("{}", smaller); // -1454759936
1
2
3

处理这种情况有很多选择。根据具体情况,这样的“环绕”转换可能是合适的。另一方面,像数字信号处理和控制系统这样的应用,通常可以采用“饱和”转换,即大于最大可能值的数字被限制为该最大值。

# TryFrom 和 TryInto

由于不清楚这样的转换应该如何表现,Rust没有为i32实现From<i64>,也没有为任何其他可能会丢失信息的数值类型之间进行转换实现From。相反,i32实现了TryFrom<i64>。

TryFrom和TryInto是From和Into的可失败版本,它们同样是相互对应的;实现了TryFrom也就意味着实现了TryInto。

它们的定义只比From和Into稍微复杂一点。

pub trait TryFrom<T> : Sized {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto<T> : Sized {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}
1
2
3
4
5
6
7
8
9

try_into()方法返回一个Result,这样我们就可以在异常情况下(比如数字太大无法放入结果类型)选择如何处理:

use std::convert::TryInto;
// 溢出时饱和处理,而不是环绕
let smaller: i32 = huge.try_into().unwrap_or(i32::MAX);
1
2
3

如果我们还想处理负数情况,可以使用Result的unwrap_or_else()方法:

let smaller: i32 = huge.try_into().unwrap_or_else(|_| {
    if huge >= 0 {
        i32::MAX
    } else {
        i32::MIN
    }
});
1
2
3
4
5
6
7

为你自己的类型实现可失败的转换也很容易。Error类型可以根据具体应用的需求,简单或复杂。标准库使用一个空结构体,除了表明发生了错误之外不提供任何信息,因为唯一可能的错误就是溢出。另一方面,更复杂类型之间的转换可能想要返回更多信息:

impl TryInto<LinearShift> for Transform {
    type Error = TransformError;
    fn try_into(self) -> Result<LinearShift, Self::Error> {
        if!self.normalized() {
            return Err(TransformError::NotNormalized);
        }
       ...
    }
}
1
2
3
4
5
6
7
8
9

From和Into用于简单转换的类型关联,而TryFrom和TryInto在From和Into转换的简单性基础上,通过Result提供了更具表达性的错误处理能力。这四个特性可以一起使用,在单个库中关联多种类型。

# ToOwned

给定一个引用,通常情况下,如果该类型实现了std::clone::Clone,可以通过调用clone来生成其引用目标的拥有所有权的副本。但是,如果你想克隆一个&str或&[i32]会怎样呢?你可能想要的是一个String或Vec<i32>,但Clone的定义不允许这样做:根据定义,克隆一个&T必须始终返回一个T类型的值,而str和[u8]是未确定大小的类型,它们甚至不是函数可以返回的类型。

std::borrow::ToOwned特性提供了一种更宽松的方式,将引用转换为拥有所有权的值:

trait ToOwned {
    type Owned: Borrow<Self>;
    fn to_owned(&self) -> Self::Owned;
}
1
2
3
4

与clone必须精确返回Self不同,to_owned可以返回任何能借用&Self的类型:Owned类型必须实现Borrow<Self>。你可以从Vec<T>借用&[T],所以只要T实现了Clone(以便我们可以将切片的元素复制到向量中),[T]就可以实现ToOwned<Owned=Vec<T>>。同样,str实现了ToOwned<Owned=String>,Path实现了ToOwned<Owned=PathBuf>等等。

# Borrow 和 ToOwned 的应用:实用的 Cow

充分利用Rust需要仔细考虑所有权问题,比如函数应该通过引用还是值来接收参数。通常你可以选择其中一种方式,参数的类型也反映了你的决定。但在某些情况下,直到程序运行你才能决定是借用还是拥有所有权;std::borrow::Cow类型(表示“写时克隆”)提供了一种解决方案。

它的定义如下:

enum Cow<'a, B: ?Sized>
where
    B: ToOwned
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}
1
2
3
4
5
6
7

Cow<B>要么借用一个指向B的共享引用,要么拥有一个可以从中借用这种引用的值。

由于Cow实现了Deref,你可以像使用指向B的共享引用一样对它调用方法:如果它是Owned,它会借用对拥有的值的共享引用;如果它是Borrowed,它直接返回所持有的引用。

你还可以通过调用to_mut方法来获取对Cow的值的可变引用,该方法返回一个&mut B。如果Cow恰好是Cow::Borrowed,to_mut会简单地调用引用的to_owned方法来获取引用目标的副本,将Cow转换为Cow::Owned,并借用对新拥有的值的可变引用。这就是该类型名称所指的“写时克隆”行为。

同样,Cow有一个into_owned方法,如果有必要,它会将引用提升为拥有所有权的值,然后返回它,在这个过程中将所有权转移给调用者并消耗掉Cow。

Cow的一个常见用途是返回一个静态分配的字符串常量或一个计算得到的字符串。例如,假设你需要将一个错误枚举转换为一条消息。大多数变体可以用固定字符串处理,但其中一些变体有额外的数据应该包含在消息中。你可以返回一个Cow<'static, str>:

use std::path::PathBuf;
use std::borrow::Cow;

fn describe(error: &Error) -> Cow<'static, str> {
    match *error {
        Error::OutOfMemory => "out of memory".into(),
        Error::StackOverflow => "stack overflow".into(),
        Error::MachineOnFire => "machine on fire".into(),
        Error::Unfathomable => "machine bewildered".into(),
        Error::FileNotFound(ref path) => {
            format!("file not found: {}", path.display()).into()
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这段代码使用Cow的Into实现来构造值。这个match语句的大多数分支返回一个指向静态分配字符串的Cow::Borrowed。但是当我们遇到FileNotFound变体时,我们使用format!来构造一个包含给定文件名的消息。这个match分支产生一个Cow::Owned值。

调用describe且不需要更改值的调用者可以简单地将Cow当作&str来处理:

println!("Disaster has struck: {}", describe(&error));
1

需要拥有所有权的值的调用者可以轻松生成一个:

let mut log: Vec<String> = Vec::new();
...
log.push(describe(&error).into_owned());
1
2
3

使用Cow有助于describe函数及其调用者将分配操作推迟到必要时进行。

编辑 (opens new window)
上次更新: 2025/03/20, 19:44:38
第12章 运算符重载
第14章 闭包

← 第12章 运算符重载 第14章 闭包→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式