Rust 2021 版本计划

2021 年 5 月 11 日 · Mara Bos 代表 Rust 2021 版本工作组

我们很高兴地宣布,Rust 语言的第三个版本 Rust 2021 计划于 10 月发布。Rust 2021 包含一些小的更改,但这些更改有望显著改善 Rust 在实践中的体验。

什么是版本?

Rust 1.0 的发布确立了 “稳定而不停滞” 作为 Rust 的核心交付成果。自 1.0 版本发布以来,Rust 的规则一直是:一旦某个功能在稳定版上发布,我们就承诺在所有未来的版本中支持该功能。

但是,有时需要对语言进行一些小的、不向后兼容的更改。最明显的例子是引入一个新的关键字,这将使具有相同名称的变量无效。例如,Rust 的第一个版本没有 asyncawait 关键字。在后面的版本中突然将这些词更改为关键字会破坏诸如 let async = 1; 之类的代码。

版本是我们用来解决此问题的机制。当我们想要发布一个原本会向后不兼容的功能时,我们会将其作为新的 Rust 版本的一部分进行发布。版本是可选的,因此现有 crate 在明确迁移到新版本之前不会看到这些更改。这意味着即使是最新版本的 Rust 也不会async 视为关键字,除非选择了 2018 或更高版本。此选择是每个 crate 在其 Cargo.toml 文件中 进行的。由 cargo new 创建的新 crate 始终配置为使用最新的稳定版本。

版本不会分裂生态系统

版本最重要的规则是,一个版本中的 crate 可以与在其他版本中编译的 crate 无缝互操作。这确保了迁移到较新版本的决定是一个“私人的决定”,crate 可以做出此决定而不会影响其他 crate。

crate 互操作性的要求意味着对我们可以在版本中进行的更改类型有一些限制。一般来说,版本中发生的更改往往是“表面上的”。所有 Rust 代码,无论版本如何,最终都会在编译器中编译为相同的内部表示。

版本迁移简单且高度自动化

我们的目标是使 crate 能够轻松升级到新版本。当我们发布新版本时,我们还会提供 自动化迁移的工具。它会对您的代码进行必要的少量更改,使其与新版本兼容。例如,在迁移到 Rust 2018 时,它会将任何名为 async 的内容更改为使用等效的 原始标识符语法r#async

自动化迁移不一定是完美的:可能存在一些仍需要手动更改的极端情况。该工具会尽力避免更改可能影响代码正确性或性能的语义。

除了工具之外,我们还维护一个版本迁移指南,其中涵盖了版本的一部分更改。本指南将描述更改并提供指向人们可以了解更多信息的链接。它还将涵盖人们应该注意的任何极端情况或细节。本指南既可以作为版本的概述,也可以作为人们在自动化工具遇到问题时的快速故障排除参考。

Rust 2021 计划进行哪些更改?

在过去的几个月中,Rust 2021 工作组审查了许多关于在新版本中包含哪些内容的提案。我们很高兴地宣布最终的版本更改列表。每个功能都必须满足两个标准才能列入此列表。首先,它们必须获得相应的 Rust 团队的批准。其次,它们的实现必须达到足够的进度,使我们有信心它们可以在计划的里程碑之前完成。

添加到 prelude

标准库的 prelude 是一个模块,其中包含每个模块中自动导入的所有内容。它包含常用的项,例如 OptionVecdropClone

Rust 编译器优先考虑任何手动导入的项,而不是 prelude 中的项,以确保添加到 prelude 不会破坏任何现有代码。例如,如果您有一个名为 example 的 crate 或模块,其中包含 pub struct Option;,则 use example::*; 会使 Option 明确引用 example 中的 Option;而不是标准库中的 Option

但是,向 prelude 添加trait可能会以微妙的方式破坏现有代码。如果也导入了 stdTryInto,则使用 MyTryInto trait 调用 x.try_into() 可能会变得模棱两可并编译失败,因为它提供了一个具有相同名称的方法。这就是为什么我们还没有将 TryInto 添加到 prelude 中的原因,因为有很多代码会以这种方式破坏。

作为解决方案,Rust 2021 将使用新的 prelude。它与当前的 prelude 相同,除了三个新添加项

默认 Cargo 功能解析器

自 Rust 1.51.0 以来,Cargo 对 新的功能解析器提供了可选支持,可以使用 Cargo.toml 中的 resolver = "2" 激活。

从 Rust 2021 开始,这将是默认设置。也就是说,在 Cargo.toml 中写入 edition = "2021" 将意味着 resolver = "2"

新的功能解析器不再合并以多种方式依赖的 crate 的所有请求的功能。有关详细信息,请参阅 Rust 1.51 的公告

数组的 IntoIterator

在 Rust 1.53 之前,只有数组的引用实现了 IntoIterator。这意味着您可以遍历 &[1, 2, 3]&mut [1, 2, 3],但不能直接遍历 [1, 2, 3]

for &e in &[1, 2, 3] {} // Ok :)

for e in [1, 2, 3] {} // Error :(

这已经是一个 长期存在的问题,但解决方案并不像看起来那么简单。仅仅 添加 trait 实现 就会破坏现有代码。 array.into_iter() 今天已经可以编译,因为由于 方法调用语法的工作原理,它会隐式调用 (&array).into_iter()。添加 trait 实现会改变其含义。

通常,我们将这种类型的破坏(添加 trait 实现)归类为“次要”且可以接受。但在这种情况下,有太多代码会被它破坏。

已经多次建议“仅在 Rust 2021 中为数组实现 IntoIterator”。但是,这根本不可能。您不能让一个 trait 实现在一个版本中存在,而在另一个版本中不存在,因为版本可以混合使用。

相反,我们决定在所有版本中添加 trait 实现(从 Rust 1.53.0 开始),但添加一个小的技巧来避免在 Rust 2021 之前出现破坏。在 Rust 2015 和 2018 代码中,编译器仍然会将 array.into_iter() 解析为像以前一样的 (&array).into_iter(),就像 trait 实现不存在一样。这适用于 .into_iter() 方法调用语法。它不会影响任何其他语法,例如 for e in [1, 2, 3]iter.zip([1, 2, 3])IntoIterator::into_iter([1, 2, 3])。这些语法将在所有版本中开始工作。

虽然为了避免破坏而需要一个小技巧令人遗憾,但我们对这种解决方案如何将版本之间的差异保持在绝对最小值感到非常满意。由于该技巧仅存在于较旧的版本中,因此在新版本中没有增加任何复杂性。

闭包中的不相交捕获

闭包会自动捕获您从其主体中引用的任何内容。例如,|| a + 1 会自动捕获对周围上下文中 a 的引用。

目前,这适用于整个结构体,即使仅使用一个字段。例如,|| a.x + 1 会捕获对 a 的引用,而不仅仅是 a.x。在某些情况下,这是一个问题。当结构体的字段已被(可变地)借用或移出时,闭包中不能再使用其他字段,因为这将捕获整个结构体,而该结构体已不再可用。

let a = SomeStruct::new();

drop(a.x); // Move out of one field of the struct

println!("{}", a.y); // Ok: Still use another field of the struct

let c = || println!("{}", a.y); // Error: Tries to capture all of `a`
c();

从 Rust 2021 开始,闭包将仅捕获它们使用的字段。因此,上面的示例在 Rust 2021 中可以正常编译。

此新行为仅在新版本中激活,因为它可能会更改字段被删除的顺序。对于所有版本更改,都有一个自动迁移可用,该迁移将更新对此有影响的闭包。它可以在闭包内部插入 let _ = &a;,以强制像以前一样捕获整个结构体。

panic! 宏的一致性

panic!() 宏是 Rust 中最广为人知的宏之一。然而,它有一些细微的意外之处,由于向后兼容性,我们不能随意更改。

panic!("{}", 1); // Ok, panics with the message "1"
panic!("{}"); // Ok, panics with the message "{}"

panic!() 宏仅在被调用时带有多个参数的情况下才使用字符串格式化。当使用单个参数调用时,它甚至不会查看该参数。

let a = "{";
println!(a); // Error: First argument must be a format string literal
panic!(a); // Ok: The panic macro doesn't care

(它甚至接受非字符串,例如 panic!(123),这很不常见且很少有用。)

一旦隐式格式化参数稳定下来,这将尤其成为一个问题。该功能将使 println!("hello {name}") 成为 println!("hello {}", name) 的简写。然而,panic!("hello {name}") 将无法按预期工作,因为 panic!() 不会将单个参数视为格式化字符串。

为了避免这种令人困惑的情况,Rust 2021 提供了一个更一致的 panic!() 宏。新的 panic!() 宏将不再接受任意表达式作为唯一参数。它将像 println!() 一样,始终将第一个参数作为格式化字符串处理。由于 panic!() 将不再接受任意有效负载,panic_any() 将成为使用格式化字符串之外的其他内容引发 panic 的唯一方法。

此外,core::panic!()std::panic!() 在 Rust 2021 中将是相同的。目前,两者之间存在一些历史差异,当开启或关闭 #![no_std] 时,这些差异可能会很明显。

保留语法

为了在未来为一些新的语法腾出空间,我们决定保留带有前缀的标识符和字面量的语法:prefix#identifierprefix"string"prefix'c'prefix#123,其中 prefix 可以是任何标识符。(除了那些已经具有含义的标识符,例如 b'…'r"…"。)

这是一个破坏性更改,因为宏目前可以接受 hello"world",它们会将其视为两个单独的标记:hello"world"。不过,(自动)修复很简单。只需插入一个空格:hello "world"

除了将这些变成标记化错误之外,RFC 尚未为任何前缀附加含义。为特定前缀赋予含义将留给未来的提案,由于现在保留了这些前缀,因此不会是破坏性更改。

以下是一些你将来可能会看到的新前缀

  • f"" 作为格式化字符串的简写。例如,f"hello {name}" 作为等效 format_args!() 调用的简写。

  • c""z"" 用于以 null 结尾的 C 字符串。

  • k#keyword 允许编写当前版本中尚不存在的关键字。例如,虽然 async 在 2015 版本中不是关键字,但此 前缀会允许我们在 2015 版本中接受 k#async 作为替代方案,同时我们等待 2018 版本将 async 保留为关键字。

将两个警告提升为硬错误

在 Rust 2021 中,两个现有的 lint 将变为硬错误。这些 lint 在旧版本中仍将保留为警告。

  • bare-trait-objects:在 Rust 2021 中,使用 dyn 关键字来标识trait 对象将是强制性的。

  • ellipsis-inclusive-range-patterns:Rust 2021 中不再接受用于包含范围模式的已弃用的 ... 语法。它已被 ..= 取代,后者与表达式一致。

macro_rules 中的或模式

从 Rust 1.53.0 开始,模式被扩展为支持在模式中的任何位置嵌套 |。这使您可以编写 Some(1 | 2) 而不是 Some(1) | Some(2)。由于以前根本不允许这样做,因此这不是一个破坏性更改。

但是,此更改也影响macro_rules。此类宏可以使用 :pat 片段说明符接受模式。目前,:pat| 不匹配,因为在 Rust 1.53 之前,并非所有模式(在所有嵌套级别)都可以包含 |。接受诸如 A | B 之类模式的宏,例如matches!() 使用类似于 $($_:pat)|+ 的内容。因为我们不想破坏任何现有的宏,所以我们没有在 Rust 1.53.0 中更改 :pat 的含义以包含 |

相反,我们将把它作为 Rust 2021 的一部分进行更改。在新版本中,:pat 片段说明符匹配 A | B

由于有时仍然希望匹配不带 | 的单个模式变体,因此添加了片段指定 :pat_param 以保留旧的行为。该名称指的是其主要用例:闭包参数中的模式。

接下来是什么?

我们的计划是在 9 月份之前合并并充分测试这些更改,以确保 2021 版本进入 Rust 1.56.0。Rust 1.56.0 随后将进入为期六周的 beta 测试阶段,之后将于 10 月 21 日作为稳定版本发布。

但是,请注意,Rust 是一个由志愿者运营的项目。我们优先考虑每个参与 Rust 开发的人员的个人福祉,而不是我们可能设置的任何截止日期和期望。如果需要,这可能意味着延迟一个版本的发布,或者放弃一个被证明太困难或压力太大而无法及时完成的功能。

也就是说,我们正在按计划进行,并且许多难题已经解决,这要归功于所有为 Rust 2021 做出贡献的人!💛


你可以在 7 月份收到关于新版本的另一个公告。届时,我们预计所有更改和自动迁移都将实现并准备好进行公开测试。

我们很快将在“Inside Rust”博客上发布有关该过程和被拒绝的提案的更多详细信息。(更正:由于缺乏带宽,最终没有发生