高级迁移策略

迁移如何工作

cargo fix --edition 的工作原理是,在你的项目上运行等效于 cargo check 的命令,并启用特殊的 lints,这些 lints 会检测在下一版本中可能无法编译的代码。这些 lints 包括如何修改代码以使其在当前版本和下一版本中都能兼容的说明。cargo fix 会将这些更改应用到源代码中,然后再次运行 cargo check 以验证修复是否有效。如果修复失败,它将撤销更改并显示警告。

将代码修改为同时兼容当前版本和下一版本,可以更轻松地增量迁移代码。如果自动化迁移未能完全成功,或者需要手动帮助,你可以在更改 Cargo.toml 以使用下一版本之前,在原始版本上进行迭代。

cargo fix --edition 应用的 lints 是一个 lint group 的一部分。例如,从 2018 迁移到 2021 时,Cargo 使用 rust-2021-compatibility lint 组来修复代码。请查看下面的部分迁移部分,了解如何使用单个 lint 来帮助迁移的技巧。

cargo fix 可能会多次运行 cargo check。例如,应用一组修复后,这可能会触发需要进一步修复的新警告。Cargo 重复此过程直到不再生成新警告。

迁移多种配置

cargo fix 一次只能处理一种配置。如果你使用 Cargo features条件编译,则可能需要使用不同的标志多次运行 cargo fix

例如,如果你的代码使用 #[cfg] 属性为不同平台包含不同的代码,你可能需要使用 --target 选项运行 cargo fix 来修复不同目标。这可能需要你在没有交叉编译的情况下将代码移动到不同的机器上。

类似地,如果你有基于 Cargo feature 的条件,例如 #[cfg(feature = "my-optional-thing")],建议使用 --all-features 标志,以便 cargo fix 迁移所有那些 feature gate 后面的代码。如果你想单独迁移 feature 代码,可以使用 --features 标志逐个迁移。

迁移大型项目或工作空间

你可以增量迁移大型项目,以便在遇到问题时更容易处理。

Cargo 工作空间中,每个包定义自己的版本,因此这个过程自然涉及到一次迁移一个包。

Cargo 包内,你可以一次迁移整个包,或者逐个迁移单独的 Cargo target。例如,如果你有多个二进制文件、测试和示例,你可以使用 cargo fix --edition 搭配特定的目标选择标志来只迁移那一个目标。默认情况下,cargo fix 使用 --all-targets

对于更高级的情况,你可以在 Cargo.toml 中为每个单独的目标指定版本,如下所示

[[bin]]
name = "my-binary"
edition = "2018"

通常这不需要,但如果你有很多目标且难以一起迁移,这是一个选项。

带破损代码的部分迁移

有时编译器建议的修复可能无效。发生这种情况时,Cargo 会报告警告,说明发生了什么以及错误是什么。但是,默认情况下,它会自动撤销所做的更改。保留代码在破损状态并手动解决问题会很有帮助。有些修复可能是正确的,而破损的修复可能“基本上”是正确的,只是需要微调。

在这种情况下,使用 cargo fix--broken-code 选项告诉 Cargo 不要撤销更改。然后,你可以手动检查错误并调查需要做什么来修复它。

另一种增量迁移项目的方法是分别应用单个修复,一次一个。你可以通过将单个 lints 作为警告添加,然后运行 cargo fix(不带 --edition 标志)或使用你的编辑器或 IDE 来应用其建议(如果它支持“Quick Fixes”)。

例如,2018 版本使用 keyword-idents lint 来修复任何冲突的关键字。你可以在每个 crate 的顶部添加 #![warn(keyword_idents)](例如在 src/lib.rssrc/main.rs 的顶部)。然后,运行 cargo fix 将仅应用该 lint 的建议。

你可以在 lint group 页面查看每个版本启用的 lints 列表,或者运行 rustc -Whelp 命令。

迁移宏

有些宏可能需要手动工作才能在下一版本中修复。例如,cargo fix --edition 可能无法自动修复生成在下一版本中无效的语法的宏。

这对于 proc macrosmacro_rules 风格的宏都可能是个问题。如果宏在同一个 crate 内使用,macro_rules 宏有时可以自动更新,但在某些情况下它无法自动更新。proc macros 通常完全无法自动修复。

例如,如果我们将包含这个(人为设计的)宏 foo 的 crate 从 2015 迁移到 2018,foo 不会自动修复。

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! foo {
    () => {
        let dyn = 1;
        println!("it is {}", dyn);
    };
}
}

当这个宏在 2015 crate 中定义时,由于宏卫生(下面讨论),它可以被任何其他版本的 crate 调用。在 2015 中,dyn 是一个普通标识符,可以无限制使用。

然而,在 2018 中,dyn 不再是有效标识符。使用 cargo fix --edition 迁移到 2018 时,Cargo 不会显示任何警告或错误。但是,从任何 crate 调用 foo 时都无法工作。

如果你有宏,建议确保你的测试完全覆盖了宏的语法。你可能还希望通过在多个版本的 crate 中导入和使用它们来测试宏,以确保它在任何地方都能正常工作。如果遇到问题,你需要仔细阅读本指南的章节,以了解如何修改代码使其在所有版本中都能工作。

宏卫生

宏使用一个称为“版本卫生”的系统,其中宏中的 token 会被标记它们来自哪个版本。这使得外部宏可以从不同版本的 crate 中调用,而无需担心从哪个版本调用。

让我们仔细看看上面的例子,它使用 dyn 作为标识符定义了一个 macro_rules 宏。如果该宏在使用了 2015 版本的 crate 中定义,那么该宏工作正常,即使它是在 2018 crate 中调用的,而在 2018 中 dyn 是一个关键字,通常会导致语法错误。let dyn = 1; 这些 token 被标记为来自 2015,编译器会记住无论代码在哪里展开,都是来自 2015。解析器会查看 token 的版本来知道如何解释它。

问题出现在将定义宏的 crate 的版本更改为 2018 时。现在,这些 token 被标记为 2018 版本,并且这些 token 将无法解析。然而,由于我们从未从我们的 crate 调用过宏,cargo fix --edition 没有机会检查并修复该宏。

文档测试

目前,cargo fix 无法更新 文档测试。在更新 Cargo.toml 中的版本后,你应该运行 cargo test 以确保一切仍然通过。如果你的文档测试使用了在新版本中不支持的语法,你需要手动更新它们。

在极少数情况下,你可以手动为每个测试设置版本。例如,你可以在三重反引号上使用 edition2018 注解来告诉 rustdoc 使用哪个版本。

生成代码

自动化修复无法应用的另一个领域是,如果你有一个 build script 在编译时生成 Rust 代码(参见代码生成了解示例)。在这种情况下,如果生成的代码在下一版本中无法工作,你需要手动更改 build script 以生成兼容的代码。

迁移非 Cargo 项目

如果你的项目没有使用 Cargo 作为构建系统,仍然可以利用自动化 lints 来辅助迁移到下一版本。你可以通过启用相应的 lint group 来启用迁移 lints,如上所述。例如,你可以使用 #![warn(rust_2021_compatibility)] 属性或 -Wrust-2021-compatibility--force-warns=rust-2021-compatibility CLI 标志

下一步是将这些 lints 应用到你的代码中。这里有几个选项

  • 手动阅读警告并应用编译器建议的修改。
  • 使用支持自动应用建议的编辑器或 IDE。例如,Visual Studio Code 搭配 Rust Analyzer 扩展具有使用“Quick Fix”链接自动应用建议的功能。许多其他编辑器和 IDE 也有类似的功能。
  • 使用 rustfix 库编写迁移工具。Cargo 内部就是使用这个库来获取编译器产生的 JSON 消息并修改源代码的。查看 examples 目录了解如何使用该库的示例。

在新版本中编写地道的代码

版本不仅仅是关于新功能和移除旧功能。在任何编程语言中,习惯用法都会随时间变化,Rust 也不例外。虽然旧代码会继续编译,但如今的代码可能使用了不同的习惯用法。

例如,在 Rust 2015 中,外部 crate 必须像这样用 extern crate 列出

// src/lib.rs
extern crate rand;

在 Rust 2018 中,不再需要包含这些项。

cargo fix 具有 --edition-idioms 选项,可以自动将一些习惯用法转换为新语法。

警告:当前的“习惯用法 lint”已知存在一些问题。它们可能会给出错误的建议,导致编译失败。当前的 lints 包括:

以下说明仅推荐给愿意克服一些编译器/Cargo bug 的勇敢者!如果你遇到问题,可以尝试使用上面描述的 --broken-code 选项,尽可能多地取得进展,然后手动解决剩余问题。

话虽如此,我们可以指示 Cargo 使用以下命令修复我们的代码片段

cargo fix --edition-idioms

之后,src/lib.rs 中带有 extern crate rand; 的行将被删除。

现在我们的代码更地道了,而且无需手动修改!