Rust 2021 Edition 计划

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

我们很高兴地宣布,Rust 语言的第三个版本,Rust 2021,计划于今年十月发布。Rust 2021 包含许多小改动,但预计它们将显著改善 Rust 的实际使用体验。

什么是 Edition?

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

然而,有时对语言进行一些不向后兼容的小改动会很有用。最明显的例子是引入一个新的关键字,这会使与该关键字同名的变量失效。例如,Rust 的第一个版本没有 asyncawait 关键字。在后续版本中突然将这些词变为关键字,就会破坏 let async = 1; 这样的代码。

Edition 是我们用来解决这个问题的机制。当我们想要发布一个否则会向后不兼容的功能时,我们会将其作为新的 Rust Edition 的一部分。Edition 是选择加入的,因此现有 crate 在显式迁移到新的 Edition 之前不会看到这些变化。这意味着即使是最新版本的 Rust,也仍然不会async 视为关键字,除非选择了 Edition 2018 或更高版本。这个选择是每个 crate 在其 Cargo.toml进行的。通过 cargo new 创建的新 crate 总是配置为使用最新的稳定版 Edition。

Edition 不会分裂生态系统

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

对 crate 互操作性的要求意味着我们可以在 Edition 中进行的更改类型受到一些限制。一般来说,Edition 中发生的更改倾向于“表面上的”。所有 Rust 代码,无论使用哪个 Edition,最终都在编译器内部编译成相同的内部表示形式。

Edition 迁移简单且高度自动化

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

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

除了工具,我们还维护一份 Edition 迁移指南,涵盖作为 Edition 一部分的变化。本指南将描述这些变化,并提供进一步学习的资源。它还会涵盖人们应该注意的任何特殊情况或细节。本指南既可以作为 Edition 的概览,也可以作为人们在使用自动化工具遇到问题时的快速故障排除参考。

Rust 2021 计划包含哪些更改?

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

prelude 中新增项

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

Rust 编译器会优先处理任何手动导入的项,而不是来自 prelude 的项,以确保向 prelude 添加内容不会破坏任何现有代码。例如,如果你有一个名为 example 的 crate 或模块,其中包含一个 pub struct Option;,那么 use example::*; 将使 Option 明确地指向来自 example 的那个,而不是标准库中的那个。

然而,向 prelude 添加一个 trait 可以以微妙的方式破坏现有代码。如果 stdTryInto trait 也被导入,那么使用 MyTryInto trait 调用 x.try_into() 可能会变得不明确并导致编译失败,因为它提供了同名的方法。这就是我们尚未将 TryInto 添加到 prelude 的原因,因为有大量代码会因此而被破坏。

作为解决方案,Rust 2021 将使用一个新的 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 实现不能只存在于一个 Edition 而不存在于另一个 Edition,因为 Edition 可以混合使用。

相反,我们决定在所有 Edition 中添加 trait 实现(从 Rust 1.53.0 开始),但增加一个小的“补丁”(hack)来避免在 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])。这些将在所有 Edition 中开始工作。

虽然需要一个小补丁来避免破坏有点遗憾,但我们对这个解决方案能将不同 Edition 之间的差异保持在最低限度感到非常满意。由于这个补丁只存在于旧的 Edition 中,新的 Edition 没有增加额外的复杂性。

闭包中的不相交捕获

闭包会自动捕获你在其函数体内部引用的任何内容。例如,|| 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 中将正常编译。

这项新行为仅在新 Edition 中启用,因为它可能改变字段的丢弃顺序。与所有 Edition 更改一样,提供了自动迁移工具,它将更新受此影响的闭包。它可以在闭包内部插入 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!() 将不再接受任意负载(payloads),panic_any() 将是唯一一种可以使用格式化字符串以外的内容引发 panic 的方式。

此外,在 Rust 2021 中,core::panic!()std::panic!() 将是相同的。目前,两者之间存在一些历史差异,在切换 #![no_std] 时可能会注意到。

保留语法

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

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

除了将这些变成 tokenization 错误之外,该 RFC 尚未为任何前缀附加含义。为特定前缀分配含义留待未来的提案,这样一来——多亏现在保留了这些前缀——这些未来的提案将不会是破坏性更改。

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

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

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

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

将两个警告升级为硬错误

两个现有的 lint 在 Rust 2021 中将成为硬错误。这些 lint 在旧的 Edition 中将保持警告级别。

  • 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 的一部分进行。在新 Edition 中,:pat 片段说明符匹配 A | B

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

接下来做什么?

我们的计划是到九月将这些更改合并并充分测试,以确保 2021 Edition 进入 Rust 1.56.0。然后 Rust 1.56.0 将进入为期六周的 beta 阶段,之后于十月 21 日作为稳定版发布。

然而,请注意 Rust 是一个由志愿者运营的项目。我们优先考虑参与 Rust 工作的所有人的个人健康,而不是我们可能设定的任何截止日期和期望。这意味着如果需要,可能会将 Edition 推迟一个版本,或者放弃一个事实证明难以或有压力及时完成的功能。

话虽如此,我们目前进展顺利,许多困难的问题已经得到解决,这要感谢所有为 Rust 2021 做出贡献的人们!💛


你可以期待七月关于新 Edition 的另一个公告。届时,我们预计所有更改和自动化迁移都将实现并准备好进行公开测试。

我们很快将在“Inside Rust”博客上发布更多关于此过程和被拒绝提案的详细信息。(更正:由于带宽不足,此事最终未能实现