历史: 当Rust的标准库引入Cow(写时复制)时,目的是抽象出可能借用或拥有的数据,而无需强制立即分配。最初考虑了Clone特性,但它仅允许生成相同类型的相同副本。对于借用的数据,如**&str**,克隆会生成另一个引用,而不是所需的可变String。ToOwned特性专门设计用于表达借用和拥有形式之间的关系,通过它的关联Owned类型。
问题: 如果Cow依赖于Clone,将Cow::Borrowed(&str)转换为可修改的拥有表示时,将需要外部转换逻辑。Clone缺乏将&str转换为String的类型级机制,迫使在构造时进行过早分配或复杂的手动状态管理。这将违反Cow的零成本抽象原则,使得无法推迟堆分配,直到实际需要修改时。
解决方案: ToOwned定义了type Owned和fn to_owned(&self) -> Self::Owned,允许**&str指定Owned = String**。这使得Cow::to_mut()能够在请求修改时懒惰地分配。如果Cow已经是Owned,它将返回对现有数据的可变引用而不进行分配。以下示例演示了这种效率:
use std::borrow::Cow; fn normalize_whitespace(input: &str) -> Cow<'_, str> { if input.contains(" ") { let cleaned = input.replace(" ", " "); Cow::Owned(cleaned) // 仅在这里分配 } else { Cow::Borrowed(input) // 零成本借用 } }
一个高吞吐量的日志处理服务需要标准化来自内存映射文件的条目中的时间戳。输入作为指向映射的**&str切片到达,但大约10%的条目需要时区调整,这需要String分配。初始实现使用了一个带有String和&str**变体的自定义枚举,需要在每个访问点进行全面的模式匹配和容易出错、冗长的手动克隆逻辑。
替代方案1:急切转换为String。 团队考虑在摄取时立即将所有输入转换为String。这种方法简化了数据模型,消除了生命周期的担忧,但带来了严重的内存开销。高峰负载期间,这使得90%从未需要修改的日志的内存使用量加倍,导致处理10GB文件时出现OOM错误。
替代方案2:使用Arc<str>进行写时复制。 另一个选项涉及使用Arc<str>进行不可变共享,并结合Arc::make_mut进行修改。虽然这提供了共享所有权语义,但为每次访问引入了原子引用计数的开销。此外,它仍然需要显式逻辑来处理从共享到可变的过渡,复杂化了借用模型而没有提供所需的方便性。
替代方案3:采用Cow<'_, str>。 团队选择Cow来抽象这两个状态。Borrowed变体直接指向内存映射,没有分配,而Owned变体则持有修改后的字符串。选择这个解决方案的原因是**to_mut()**在首次发生修改时推迟分配,保持只读路径的零成本,同时提供统一的API。
结果: 解析器保持高吞吐量,仅使用200MB的实际堆分配处理10GB的日志文件。通过利用Cow,系统消除了手动状态跟踪,维护了并行处理的Send和Sync属性,并相比自定义枚举方法降低了60%的代码复杂度。
into_owned通过值返回ToOwned::Owned,这需要在编译时已知的大小以分配栈空间。虽然Cow可以用未定类型如str包装,但Owned类型(String)是已定大小的。候选人常常混淆Cow<', T>与Cow<', &T>,试图为引用而不是借用类型实现特性。如果没有ToOwned::Owned上的Sized约束,编译器无法构建into_owned的返回值,因为它将试图直接返回一个未定大小的str,而不是具有已定大小的String容器。
Cow实现了Borrow<Borrowed>,其中Borrowed: ToOwned,允许用**&str查找Cow<String>。然而,Borrow施加了严格的契约:如果两个值通过Eq相等,则它们必须生成相同的哈希值。候选人常常为Cow实现自定义PartialEq**(例如,不区分大小写的比较),同时保留标准的Hash实现。这违反了契约,因为两个Cow值可能在自定义逻辑下相等,但如果Hash实现看到原始字节,则哈希值可能不同。这导致在HashMap查找失败时,键似乎存在但无法找到。
要构建Borrowed变体,Cow需要一个生命周期为**'a的引用&'a B**。一个通用的Default实现需要生成一个有效的**'static引用(例如&'static str用于""),但&str本身并不实现Default**,因为没有可以返回的通用引用值。候选人常常建议默认使用Cow::Borrowed(""),但这要求B上有一个**'static生命周期约束,或专门化,在稳定的Rust中不可用。因此,标准库要求ToOwned::Owned: Default**,强迫Cow::Owned(String::new())(一次分配)即使对于空的默认值。候选人遗漏了这个区别,因为他们将特定范围内字符串字面量的可用性与引用的通用Default实现混淆。