Rust 2021 edition预计今年下半年发布。我用nightly版本把一个旧的Rust项目迁移到了2021 edition。本文是迁移实战记录。需要说明的是2021 edition还在开发中。最终发布时可能会有变化。

一、为什么要迁移

先说说为什么要迁移到2021 edition。

我们有一个Rust项目。用的是2018 edition。代码量大概五万行。已经运行了两年多。一直很稳定。

Rust 2021 edition的预览版出来之后。我看了一下新特性。觉得有几个特性很有用。比如IntoIterator for arrays。更智能的闭包捕获。默认的Cargo特性解析。还有一些语法的改进。

虽然2021 edition还没正式发布。但是我想提前体验一下。看看迁移的难度有多大。为正式发布之后的迁移做准备。

于是我在一个分支上。用nightly版本的Rust。把项目迁移到了2021 edition。本文就是这次迁移的记录。

二、2021 edition的主要变化

先说说2021 edition有哪些主要变化。

1. 闭包捕获的改进

这是2021 edition最大的变化之一。在2018 edition中。闭包会捕获整个变量。即使只用到了变量的一个字段。也会捕获整个变量。这有时候会导致借用问题。

在2021 edition中。闭包可以只捕获用到的字段。而不是整个变量。这样可以减少不必要的借用。让一些之前编译不过的代码能编译通过。

比如你有一个结构体。有a和b两个字段。闭包里只用到了a。在2018 edition中。闭包会捕获整个结构体。导致b也被借用了。在2021 edition中。闭包只捕获a。b不受影响。

2. IntoIterator for arrays

在2018 edition中。数组不能直接用for循环遍历。因为数组没有实现IntoIterator。需要用.iter()或者.into_iter()。

在2021 edition中。数组实现了IntoIterator。可以直接用for循环遍历数组。比如for x in [1, 2, 3]。这在以前是不行的。

这个变化虽然小。但是很实用。让数组的使用更方便了。

3. Cargo特性解析的改进

在2018 edition中。Cargo的特性解析是版本1。有一些问题。比如依赖的特性会被统一启用。可能导致一些意外的行为。

在2021 edition中。默认使用版本2的特性解析。版本2的解析更精确。不会意外启用不需要的特性。可以减少编译时间。也能避免一些bug。

4. 语法的小改进

还有一些语法的小改进。比如panic!宏的行为更一致了。比如reserved syntax的预留。为未来的语法做准备。

这些改进虽然不大。但是能让Rust用起来更顺手。

三、迁移的步骤

迁移的步骤其实很简单。

第一步:切换到nightly版本

因为2021 edition还没正式发布。所以需要用nightly版本的Rust。我用rustup安装了nightly版本。然后用cargo +nightly build来编译。

第二步:修改Cargo.toml

在Cargo.toml里。把edition = "2018"改成edition = "2021"。就这么简单。

第三步:编译和修复

然后运行cargo build。看有哪些编译错误。一个个修复。

大部分代码是不需要改的。因为edition的变化是向后兼容的。只有少数代码需要调整。

四、遇到的问题和解决方法

迁移过程中遇到了一些问题。这里记录一下。

问题1:闭包捕获变化导致的编译错误

这是最常见的问题。因为闭包捕获的方式变了。有些代码在2018 edition中能编译。在2021 edition中编译不过。

比如有一段代码。闭包里用到了结构体的一个字段。但是闭包结束之后。又用到了结构体的另一个字段。在2018 edition中。因为闭包捕获了整个结构体。所以闭包结束之后。结构体的借用就结束了。可以用另一个字段。

但是在2021 edition中。闭包只捕获了一个字段。这本来是好事。但是有些代码依赖了旧的捕获行为。比如在闭包里drop整个变量。但是因为只捕获了一个字段。drop的行为变了。

解决方法是。如果需要捕获整个变量。可以在闭包里加一句let _ = &var。这样就会捕获整个变量。或者重构代码。不依赖旧的捕获行为。

我们项目里有三处这样的代码。都改成了显式捕获整个变量。

问题2:数组IntoIterator的歧义

数组实现了IntoIterator之后。有些代码出现了歧义。

比如有一段代码。调用一个泛型函数。参数是数组。在2018 edition中。数组不会自动intoiter。所以调用的是某个版本。在2021 edition中。数组可以intoiter了。导致类型推断出现了歧义。

解决方法是显式指定类型。或者用.iter()明确表示是引用迭代。

我们项目里有两处这样的问题。都加了显式的类型标注。

问题3:新的lint警告

2021 edition有一些新的lint。有些代码会产生新的警告。

比如有一个lint是关于unused结果的。在2021 edition中更严格了。有些在2018 edition中不警告的代码。在2021 edition中会警告。

解决方法是处理这些警告。要么使用结果。要么显式忽略。我们项目里有十几处这样的警告。都处理了。

问题4:依赖库不兼容

有些依赖库还没有适配2021 edition。在2021 edition下编译会有警告或者错误。

我们的项目有几个依赖库出现了这个问题。解决方法是。如果是公开的库。可以等作者更新。或者提交PR帮助更新。如果是自己的库。就自己更新。

我们有一个自己写的库。花了半天时间适配了2021 edition。其他公开的库。大部分已经有了2021 edition的支持。或者在nightly下能正常编译。

五、迁移后的感受

迁移完成之后。说说感受。

1. 迁移难度不大

整体来说。迁移的难度不大。五万行代码。花了两天时间就迁移完了。大部分代码不需要改。只有少数地方需要调整。

这得益于Rust edition的设计。edition之间是向后兼容的。而且可以在同一个项目中混合使用不同edition的依赖。所以迁移的风险很小。

2. 新特性很实用

迁移到2021 edition之后。用了一些新特性。确实很实用。

比如数组的IntoIterator。写代码的时候更方便了。不用每次都写.iter()。比如闭包的字段级捕获。有些之前需要绕一下的代码。现在可以直接写了。

这些特性虽然不大。但是能提升开发体验。让代码更简洁。

3. 编译速度有提升

因为Cargo特性解析的改进。编译速度有一些提升。我们的项目编译时间减少了大概10%。虽然不是很多。但是聊胜于无。

4. nightly版本不稳定

因为用的是nightly版本。有时候会遇到编译器的bug。或者API的变化。有一次更新nightly之后。代码突然编译不过了。查了半天才发现是编译器的一个bug。后来又更新了一个版本就好了。

所以如果是生产环境。不建议现在就迁移到2021 edition。等正式发布之后再迁移更稳妥。

六、迁移建议

如果你也准备迁移到2021 edition。我有几个建议。

1. 等正式发布

如果你是生产项目。建议等2021 edition正式发布之后再迁移。nightly版本可能有bug。而且API可能会变化。现在迁移有风险。

2. 先在分支上试

可以先在一个分支上试迁移。看看有哪些问题。评估一下迁移的成本。等正式发布之后。再合并到主分支。

3. 用cargo fix

Rust提供了cargo fix工具。可以自动修复一些edition迁移的问题。运行cargo fix --edition。它会自动帮你改一些代码。减少手动修改的工作量。

我们迁移的时候用了cargo fix。自动修复了大部分问题。剩下的少数问题手动改。

4. 充分测试

迁移之后一定要充分测试。虽然edition的变化是兼容的。但是有些行为的变化可能导致逻辑错误。特别是闭包捕获的变化。可能会影响drop的顺序。导致一些隐蔽的bug。

我们迁移之后。跑了全部的单元测试和集成测试。还做了一些手动测试。确保没有问题。

七、写在最后

Rust 2021 edition是一次不大的更新。没有革命性的变化。但是有很多实用的改进。能提升开发体验。

迁移的难度不大。五万行代码两天就能迁完。大部分代码不需要改。只有少数地方需要调整。

如果你是Rust开发者。建议关注一下2021 edition。等正式发布之后。可以考虑迁移。新特性还是很值得体验的。

最后用一句话结束本文:"Rust的edition机制。让语言可以不断进化。同时保持稳定。这是Rust最棒的设计之一。"愿每一个Rust开发者都能享受语言进化带来的便利。