Rust 2021 版计划于 10 月发布,将带来哪些新特性?

Mara Bos AI前线 今天
图片
作者 | Mara Bos
策划 | 蔡芳芳
Rust 语言的第三版,即 Rust 2021,计划在 10 月发布。Rust 2021 包含了一些微小的变化,尽管如此,在实际应用中,它们仍有望显著改善 Rust 的感觉。
1什么是版本?

作为 Rust 的更新原则, Rust1.0 的发布确立了一个“稳定前进”(stability without stagnation)。从 1.0 版本开始,Rust 的规则是,一旦某个特性在稳定版上发布,我们将保证在以后的所有版本中都支持它。

但是,有些时候,能够对语言做一些不向后兼容的小改动是很有用的。一个显而易见的例子是引入一个新的关键字,它会使同名变量无效。比如,Rust 的第一个版本中就没有 asyncawait 这两个关键字。如果这些关键字在以后的版本中突然变成关键字,那么就会破坏类似 let async = 1; 的代码。

版本就是我们解决这个问题的机制。当我们想要发布一个本来是向后不兼容的特性时,我们就把它作为新的 Rust 版本的一部分。版本是可以选择的,因此现有包(crate)不会看到这些更改,除非明确地将其迁移到新版本上。这就是说,除非选择 2018 版或更新版本,即使是最新版本的 Rust 也不会把 async 作为关键字。对于每个包,这个选择都是 Cargo.toml 的一部分。cargo new创建的新包始终配置为使用最新稳定版本。

2版本不会分裂生态系统

对于版本来说,最重要的规则是,一个版本中的包可以与其他版本中编译的包无缝地互操作。这样就保证了将包迁移到一个更新版本的决策是一个“私有决策”,这个决策可以在不影响他人的情况下进行。

对包互操作性的需求意味着对我们在一个版本中所能做的各种更改有一些限制。一般而言,一个版本中发生的更改往往是“皮毛”。但无论哪种版本,所有的 Rust 代码最终都会被编译成编译器中相同的内部表示。

3版本迁移很容易,并且基本上是自动化

我们的目标是使包能够轻松地升级到新的版本。在发布新版本时,我们还提供了 自动迁移的工具。对代码进行一些必要的小改动,使之与新版本兼容。举例来说,在迁移到 Rust 2018 时,它将所有称为 async 的东西都使用了等效的原始标识符语法:r#async

但自动迁移并非十全十美:可能还是有一些情况仍然需要手动更改。该工具竭力避免更改语义,以避免影响代码的正确性或性能。

除了工具外,我们还维护一个版本迁移指南,其中包括版本中包含的变更。该指南将描述这些变化,并为人们提供指导,使他们能够了解更多。同时也会包括任何需要人们注意的边角案例或细节。该指南既可以作为版本的概述,也可以在人们遇到自动化工具的问题时,作为快速故障诊断的参考。

4Rust 2021 版计划有哪些变化?

Rust2021 工作组在过去几个月里已经就新版本应该包含的内容研究了很多建议。我们很高兴地公布最终的版本变更清单。每个特性都必须满足两个标准才能进入这个列表。首先,它们必须得到相应的 Rust 团队的批准。其次,它们的实施必须足够深入,让我们确信,它们会在计划的里程碑之前按时完成。

对 prelude 的补充

标准库的 prelude 模块包含了在每个模块中自动导入的所有内容。其中包含诸如 OptionVecdropClone等常用项目。

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

但是,将 trait 添加到 prelude 中,将以一种微妙的方式破坏现有代码。用 MyTryInto trait 调用 x.try_into() 可能会模棱两可,如果同时导入 stdTryInto,则无法进行编译,因为它提供了一个同名的方法。正因为如此,我们还没有把 TryInto 添加到 prelude 中,因为有许多代码都会以这种方式被破坏。

Rust 2021 将使用一个新的 prelude 作为解决方案。和现在一样,只增加了三项新内容:

  • std::convert::TryInto

  • std::convert::TryFrom

  • std::iter::FromIterator

默认 Cargo 特性解析器

从 Rust 1.51.0 开始,Cargo 为新的特性解析器提供了选项支持,可以在 Cargo.toml 中用 resolver = "2"来激活。

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

新的特性解析器不再合并以多种方式依赖的包的所有请求的特性,请详见 Rust 1.51 的公告。

用于数组的 IntoIterator

在 Rust1.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 实现会改变这个含义。

一般情况下,我们将这种破坏(增加特性的实现) 归结为“次要”,这是可以接受的。但是在这种情况下,太多的代码将被破坏。

有很多人建议“在 Rust 2021 中只对数组实现 IntoIterator”。但是,这完全不可能。你无法在一个版本中实现某个特性,而不在另一个版本中实现,因为版本可能是混合的。

取而代之的是,我们决定将 trait 实现 (从 Rust1.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])。所有版本中都会有这些特性。

尽管这需要一点小技巧才能避免破坏,这是一种可耻的做法,但是我们很高兴这个解决方案能将版本间的差异降到最低。因为这一小技巧只存在于旧版本中,所以不会在新版本中增加复杂性。

闭包中不相交的捕获

闭包(Closure)会自动捕获你在其主体中引用的任何内容。例如,|| 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 literalpanic!(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,其中前缀可以是任何标识符。(除了那些已经有意义的,如 b'...'r"..."。)

这是一个重大更改,因为宏目前可以接受 hello"world",它们会将其视为两个单独的标记:hello"world"。但(自动)修复方法非常简单。只需插入一个空格:hello "world"

RFC 除了将这些变成标记化错误外,还没有给任何前缀添加意义。赋予特定前缀的意义将留给未来的建议,由于现在保留了这些前缀,所以它们不会有大的变化。

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

  • f"" 作为格式字符串的简称。例如,f"hello {name}" 作为相当于 format_args!() 调用的简写。

  • c"" 或 z"" 表示空位的 C 语言字符串。

  • k#keyword 允许编写在当前版本中还不存在的关键字。例如,虽然 async 在 2015 版中不是一个关键字,但这个前缀可以让我们在 2015 版中接受 k#async 作为替代的同时,等待 2018 版将 async 作为一个关键词。

将两个警告提升为硬错误

两个已有 lint 正在成为 Rust 2021 中的硬错误。在旧版本中,这些 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 之前,不是所有的模式(在所有嵌套层)都可以包含 |。像 matches!() 这样的宏接受 A | B 这样的模式,使用一些与 $($_:pat)|+ 相似的内容。由于不希望破坏任何现有宏,因此在 Rust 1.53.0 中未更改 :pat 的意思以包含 |

相反,作为 Rust 2021 的一部分,我们将做出这样的改变。对于新版本,:pat 片段说明符将匹配 A | B

因为在某些时候,人们仍然希望不使用 | 来匹配单一的模式变体,所以添加了指定的 :pat_param 片段来保留旧的行为。这一名称指的是其主要用例:闭包参数中的模式。

5接下来是什么?

我们的计划在 9 月前对这些修改进行合并并进行全面测试,以确保 2021 版能进入 Rust 1.56.0。Rust 1.56.0 随后会进行 6 个星期的测试,之后在 10 月 21 日发布为稳定版。

但是要注意的是,Rust 是由志愿者运作的项目。与我们可能设定的任何截止日期和期望相比,我们更重视为 Rust 工作的每个人的个人福利。如果需要的话,这就意味着要推迟发布,或者放弃一个难以实现或太过紧张而无法及时完成的的特性。

也就是说,我们正在按计划进行,许多难题已经迎刃而解,这要感谢所有为 Rust 2021 做出贡献的人们!

作者介绍:

Mara Bos,Rust 2021 版工作组代表。

原文链接:

https://blog.rust-lang.org/2021/05/11/edition-2021.html


图片

你也「在看」吗?👇