# 加餐|Rust2021版次问世了! 你好,我是陈天。 千呼万唤始出来的 Rust 2021 edition(下称版次),终于伴随着 1.56 版本出来了。在使用 `rustup update stable` 完成工具链的升级之后,小伙伴们就可以尝试着把自己之前的代码升级到 2021 版了。 具体做法很简单: 1. `cargo fix --edition` 2. 修改 Cargo.toml,替换 edition = “2021” 3. `cargo build` / `cargo test` 确保一切正常 在做第一步之前,记得先把未提交的代码提交。 如果你是初次涉猎 Rust 的同学,可能不清楚 Rust 中“版次”的作用,它是一个非常巧妙的、向后兼容的发布工具。 不知道在其它编程语言中有没有类似的概念,反正我所了解的语言,没有类似的东西。C++ 虽然允许你编译 lib A 时用 --std=C++17,编译 lib B 时用 --std=C++20,但这种用法有不少局限,用起来也没有版次这么清爽。我们先对它的理解达成一致,再聊这次“版次”更新的重点内容。 在 Rust 中,版次之间可能会有不同的保留字和缺省行为。比如 2018的 async / await / dyn,在 2015 中就没有严格保留成关键字。 假设语言在迭代的过程中发现 actor 需要成为保留字,但如果将其设置为保留字就会破坏兼容性,会让之前把 actor 当成普通名称使用的代码无法编译通过。怎么办呢?升级大版本,让代码分裂成不兼容的 v1 和 v2 么?这个问题是令所有语言开发者头疼的事情。 语言总是要发展的,总会从不完善到完善,所以,一开始考虑不周,后来不得不通过破坏性更新来弥补的事情,屡见不鲜。 升级大版本号,是之前处理这类问题的惯常手段。 然而,对于库的作者来说,如果他不想升级大版本或者受限于某些原因无法很快升级,最终,要么是使用这个库的开发者只好坚守在 v1,要么是使用这个库的开发者不得不找到对应的和 v2 兼容的替代品。但无论哪种方式,整个生态环境都会受到撕裂。 Rust 通过“版次”非常聪明地解决了这个问题。库的作者还是以旧的版次发布他的代码,使用库的开发者可以选择他们想使用最新的版次,二者可以完全不一致,**编译时,Rust 编译器以旧的版次的功能编译旧的库,而以新的版次编译使用者的代码**。 看一个实际例子吧。在 [crates.io](http://crates.io) 里我随便搜了一个最后更新止步于三年前的库 [rbpf](https://crates.io/crates/rbpf)。看它的 [Cargo.toml](https://github.com/qmonnet/rbpf/blob/master/Cargo.toml),这是个 2015 版次的库(不声明版次就意味着 2015),和现在的代码断了两代。我们来尝试创建一个 2021 版次的 crate,同时引入这个库,以及 2018 版次的 futures 库,看有没有问题。 首先,确保你的 Rust 升级到了 1.56。然后 `cargo new test-rust-edition`。在生成的项目里,为 Cargo.toml 加入: ```rust [package] name = "test-rust-edition" version = "0.1.0" edition = "2021" [dependencies] rbpf = "0.1.0" futures = "0.3" ``` 这里我故意让两个本来是不兼容的 crate 放在一起看看是否可以协同工作。futures 使用了 async/await,这是 Rust 2018 才引入的关键字,但 rbpf 使用的 2015 版次。 修改好 Cargo.toml 后,我们在 src/main.rs 中拷入: ```rust use futures::executor::block_on; fn main() { // This is the eBPF program, in the form of bytecode instructions. let prog = &[ 0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov32 r0, 0 0xb4, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // mov32 r1, 2 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // add32 r0, 1 0x0c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // add32 r0, r1 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit ]; // Instantiate a struct EbpfVmNoData. This is an eBPF VM for programs that // takes no packet data in argument. // The eBPF program is passed to the constructor. let vm = rbpf::EbpfVmNoData::new(Some(prog)).unwrap(); block_on(async move { dummy(vm.execute_program().unwrap()).await; }); } async fn dummy(result: u64) { println!("hello world! Result is {} (should be 0x3)", result); } ``` 这个代码在做什么我们不用关心,只需要关心它能不能在 2021 版次的 crate 里跑起来,`cargo run` 后,发现 rbpf 和 futures 融洽地处在了一起。 一份代码,使用了三个版次的代码,却能够无缝对接,我们使用的时候甚至可以不用关心谁是什么版次,你说厉害不厉害? 所以你看,版次起到了防火墙的作用,使得整个生态系统不用分裂,大家无需改动,依旧能够各司其职。这就是版次对 Rust 最大的贡献。如果你经历过 Python2 到 Python3 升级过程中的巨大阵痛,那应该能够非常感激 Rust 引入了这么个非常重要的概念。 ## Rust 2021 包括了什么新东西? 在你理解 Rust 2021 版次的意义之后,再来看看对我们影响最大的几个更新。 ### 闭包的不相交捕获 在 2021 之前,哪怕你只用到了其中一个域,闭包也需要捕获整个数据结构,即使是引用。但是 2021 之后,闭包可以只捕获需要的域。 比如下面的[代码](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f7428fb00bd8648808e82618e5e8916a): ```rust struct Employee { name: String, title: String, } fn main() { let tom = Employee { name: "Tom".into(), title: "Engineer".into(), }; drop(tom.name); println!("title: {}", tom.title); // 之前这句不能工作,2021 可以编译 let c = || println!("{}", tom.title); c(); } ``` 闭包的不相交捕获对我们使用的好处是,那些闭包中捕获了结构体的一部分字段,而其它地方又用了另一部分与之不相交的字段,原本在 2018 中是编译不过的,你只能 clone() 这个结构体满足双方的需要,现在可以编译通过。 ### feature resolver 依赖管理是一个难题,其中最困难的部分之一就是在依赖两个不同的包时,选择要使用的依赖版本。这里指的不仅包括其版本号,还包括为该软件包启用或未启用的功能(feature)。因为Cargo 的默认行为是在依赖中多次引用单个包时合并所用到的功能。 例如,假设你有一个名为 Foo 的 crate,其中有 A 和 B 两个功能,该依赖项被包 bar 和 baz 使用,但 bar 依赖 Foo + A,而 baz 依赖 Foo + B。Cargo 会合并这两个功能并编译 Foo + A B。 ![](https://static001.geekbang.org/resource/image/b8/53/b8601e7c005ff666d1597dd174346d53.jpg?wh=2044x1332) 这确实有一个好处,你只需要编译一次 Foo,就可以被 bar 和 baz 使用。但是,如果 A 和 B 不应该一起编译呢?如果你对这样的场景感兴趣,可以看下面的 Rust 1.51 编译策略的链接。这是 Rust 一个长期存在并困扰社区的问题。 之前 Rust 1.51 终于提供了新的方法,通过不同的[编译策略](https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver)解决这一问题。如今,这个策略已经成为 2021 的缺省行为,它会带来一些编译速度的损失,但会让编译结果更加精确。 ### 新的 prelude 任何语言都会缺省引入某些命名空间下的一些非常常见的行为,这样让开发者使用起来很方便。Rust 也不例外,它会缺省引入一些 trait 、数据结构和宏,比如我们使用的 From / Into 这样的 trait、Vec 这样的数据结构,以及 println! / vec! 这样的宏。这样在写代码的时候,就不需要频繁地使用 use。 在 2021 版次中,TryInto、TryFrom 和 FromIterator 默认被引入到 prelude 中,我们不再需要使用 use 声明了。比如现在下面的语句就没必要了,因为 prelude 已经包含了: ```rust use std::convert::TryFrom; ``` ## 小结 总的来说,Rust 2021 不是一个大的版次更新,里面只包含了少量和之前版本不兼容的地方。未来 3 年,Rust 都将稳定在这个版次上。 也许你会不理解:搞这么大动静,就这?但这正是 Rust 当初设计用心良苦的地方。 三年内,以 6 周为单位,不断迭代新的功能,风雨无阻,但不引入破坏性更新,或者用某些编译选项将其隔离,使用者必须手工打开(比如 resolver = “2”);三年期满,升级版次,一次性把这三年内潜在的破坏性更新,以及可预见的未来会引入的破坏性更新(比如保留新的关键字),通过版次来区隔。 版次中出现的大动作越少,就说明语言越趋向成熟。 好,关于 2021 版次的介绍就到这里,还有一些其它的修改,这里我就不赘述了,感兴趣的可以看[发布文档](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html#rust-2021)。这门课的代码仓库 [tyrchen/geektime-rust](https://github.com/tyrchen/geektime-rust) 也随之升级到了 2021 版次,具体修改你可以看这个 [pull request](https://github.com/tyrchen/geektime-rust/pull/3)。