You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
9.1 KiB
Markdown

2 years ago
# 加餐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)。