这篇博客文章提出添加一个名为Claim
的第三个特性,与Copy
和Clone
并存。Claim
特性的目标是改善 Rust 现有的分裂状态,其中类型被归类为Copy
(适用于安全进行memcpy
的“普通旧数据”)和Clone
(适用于需要执行自定义代码或具有析构函数的类型)。这种分裂在 Rust 中运行良好,但随着时间的推移也暴露出一些缺点,包括维护风险、性能隐患以及(有时相当显著的)人体工程学问题和用户困惑。
TL;DR
- 添加新的
Claim
特性:细化Clone
以识别“廉价、无错且透明”的克隆(定义如下,但明确排除分配)。显式调用x.claim()
被认为是廉价的,并且易于与可能并非如此的x.clone()
调用区分开来,这使代码更易于理解并解决了现有的维护风险。 - 修改借用检查器:在使用稍后将使用的值的位置时插入
claim()
调用。例如,对于变量y: Rc<Vec<u32>>
,如果稍后再次使用y
,则赋值x = y
将被转换为x = y.claim()
。这解决了当前 Rust 中引用计数值的人体工程学问题和用户困惑,特别是与闭包和异步块相关的问题。 - 最终,将
Copy
与“移动”完全分离:首先发出警告(在当前版本中),然后在 Rust 2027 中变为错误。简而言之,除非y: Claim
,否则x = y
将移动y
。大多数Copy
类型也将是Claim
,因此这在很大程度上是向后兼容的,但它将使我们能够排除y: [u8; 1024]
之类的情况,并将Copy
扩展到Cell<u32>
或迭代器等类型,而不会引入微妙的错误。
对于某些代码,自动调用Claim
可能是不受欢迎的。为此,提议创建一个“默认允许”的automatic-claim
lint, crate 或模块可以选择加入,以便所有“声明”都可以显式进行。
步骤 1:引入显式的Claim
特性
当前代码中,仅通过map.clone()
无法得知其性能特征,因为克隆操作可能只是复制一个大型映射或增加引用计数。
单一克隆适用于所有情况会产生维护风险:在编写代码时,开发人员通常知道给定值是“廉价克隆”还是“昂贵克隆”,但随着代码的生命周期,此属性可能会发生变化。例如,变量map
的类型可能从Rc<HashMap<K, V>>
重构为HashMap<K, V>
,这会导致map.clone()
的性能特征发生很大变化,甚至可能影响程序的语义。
提议:引入显式的Claim
特性以区分“廉价、无错、透明”的克隆:新的Claim
特性是Clone
的子特性,表明克隆是廉价的(在 O(1)时间内完成,避免复制超过几个缓存行)、无错的(在任何情况下都不会遇到失败,甚至不会 panic 或中止,且不允许内存分配)和透明的(新旧值在公共 API 方面的行为相同)。Claim
特性可以定义为trait Claim: Clone { fn claim(&self) -> Self { self.clone() } }
。
步骤 2:在赋值中声明值
在 Rust 中,除非值的类型实现Copy
特性,否则在访问时会移动值。这意味着对于引用计数的map: Rc<HashMap<K, V>>
,使用map
后就不能再使用它了。
并非所有的内存复制都应该是“静默的”:当前的规则存在一些问题,例如x = y
在运行时可能会导致意外情况,对于像[u8; 1024]
这样的类型,简单的调用可能会复制大量数据;同时,x = y.clone()
或x = y.claim()
这样的代码会增加视觉混乱,分散读者的注意力。
一些应该实现Copy
的东西却没有实现:当前的规则意味着添加Copy
实现可能会创建正确性隐患,例如许多迭代器类型std::ops::Range<u32>
和std::vec::Iter<u32>
实际上可以安全地进行memcpy
,但由于担心引入微妙的错误而未实现Copy
。
克隆/复制规则与闭包的交互非常差:闭包和异步块是克隆/复制相关的最大困惑来源,将引用计数值与闭包结合对于新用户来说是一个很大的障碍。例如在处理 CloudFlare 代码库中的上下文对象时,目前的处理方式非常繁琐。
“自动声明”来解决问题:提议修改借用检查器,以便在需要时自动调用claim
。例如,表达式x = y
如果y
稍后将再次使用,将自动转换为x = y.claim()
。闭包在捕获环境变量时也会尊重自动声明。自动声明不适用于变量的最后一次使用,以避免不必要的引用计数。如果y
的类型不实现Claim
,则会给出适当的错误。
步骤 3:停止使用Copy
来控制移动
添加“自动声明”解决了必须调用clone
的人体工程学问题,但仍然意味着任何Copy
类型都可以被复制。真正的目标应该是将“可以进行内存复制”和“可以自动复制”分离。在 Rust 2024 及之前,当x = y
复制Copy
但不是Claim
的值时会发出警告,在 Rust 2027 中,这将成为一个硬错误,规则仅与Claim
特性相关。
常见问题
- 质疑提议:复制/克隆分裂在 Rust 中已经存在很长时间,但此更改的影响总体上是积极的,大多数代码将获得更少的混乱和更清晰的错误消息,而不会影响可靠性或性能。对于需要的项目,可以启用 lint 来关注性能。
- 哪些代码会禁用
#[deny(automatic_claims)]
:这是一个有趣的问题,最初认为与“高级、面向业务逻辑的代码”和“低级系统软件”的区别有关,但实际上不确定,可能是一个相当小的、专业化的项目集合。 - 代码关注引用计数的情况:设置
#![deny(automatic_claims)]
可以明确声明项目会仔细跟踪引用计数,避免性能隐患,并在后续版本中避免与Copy
相关的错误。 - 与 RFC 936 的关系:此提议是针对 RFC 936 中提出的问题的另一种替代方案,虽然之前决定不进行拆分,但此提议解决了相同的问题并且有 10 年的经验支持。
- “配置文件 lint”是否会分裂 Rust:从技术上讲,lint 自始至终都存在,它们定义了 Rust 的“子集”,而不是改变它。“配置文件模式”可能会降低添加语法糖的成本,但不应改变 Rust 的本质。
- 如何判断人体工程学更改是否“值得”:应编写一些设计原则,如 Aaron Turon 在“人体工程学倡议”博客文章中提出的三个轴(适用性、功率、上下文依赖性)来分析和管理推理足迹,以限制自动声明的影响。
- 显式闭包自动声明语法:Josh 提出了注释闭包以表示自动捕获的想法,作者认为这是一个有吸引力的概念,但尚未看到具体提案。
- 显式闭包捕获子句:作者喜欢在闭包上使用显式捕获子句的想法,可以更明确地控制捕获的内容,这是解决闭包捕获无明确形式问题的一种方式。
- 名称
Claim
的由来:作者最初认为是 Jonathan Kelley 建议的Capture
,但最终使用了Claim
,并认为如果要采取实际行动,需要进行适当的讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。