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 的新版本的一部分发布。版本是可选的,因此现有的板条箱在显式迁移到新版本之前不会看到这些更改。这意味着即使是 Rust 的最新版本也不会将 async 视为关键字,除非选择了 2018 或更高版本。此选择是在每个板条箱 作为其 Cargo.toml 的一部分 进行的。由 cargo new 创建的新板条箱始终配置为使用最新的稳定版本。

版本不会拆分生态系统

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

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

版本迁移很容易,并且很大程度上是自动化的

我们的目标是让板条箱轻松升级到新版本。当我们发布新版本时,我们还提供 工具来自动化迁移。它会对你的代码进行一些小的更改,以使其与新版本兼容。例如,当迁移到 Rust 2018 时,它会将任何名为 async 的东西更改为使用等效的 原始标识符语法r#async

自动迁移并不一定完美:可能有一些特殊情况仍然需要手动更改。该工具尽力避免对语义进行更改,这些更改可能会影响代码的正确性或性能。

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

Rust 2021 计划进行哪些更改?

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

对 prelude 的添加

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

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

但是,将特征添加到 prelude 会以一种微妙的方式破坏现有的代码。如果 stdTryInto 也被导入,那么使用 MyTryInto 特征对 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"

新的特征解析器不再合并对以多种方式依赖的板条箱的所有请求的特性。有关详细信息,请参阅 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 :(

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

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

人们多次建议“只在 Rust 2021 中为数组实现 IntoIterator”。但是,这根本不可能。你不能让一个特征实现存在于一个版本中,而在另一个版本中不存在,因为版本可以混合使用。

相反,我们决定在所有版本中添加特征实现(从 Rust 1.53.0 开始),但在 Rust 2021 之前添加一个小技巧来避免破坏。在 Rust 2015 和 2018 代码中,编译器仍然会将 array.into_iter() 解析为 (&array).into_iter(),就像以前一样,就好像特征实现不存在一样。这适用于 .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() 将成为使用除格式化字符串以外的其他内容引发恐慌的唯一方法。

此外,在 Rust 2021 中,core::panic!()std::panic!() 将是相同的。目前,这两个宏之间存在一些历史差异,这在切换 #![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 关键字来标识特征对象 将是强制性的。

  • 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 将进入为期六周的测试版,之后它将在 10 月 21 日发布为稳定版。

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

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


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

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