Rust 1.51.0 发布

2021 年 3 月 25 日 · Rust 发布团队

Rust 团队很高兴地宣布 Rust 的新版本 1.51.0。Rust 是一种旨在赋能所有人构建可靠且高效软件的编程语言。

如果您之前通过 rustup 安装了 Rust,则只需执行以下操作即可升级到 Rust 1.51.0

$ rustup update stable

如果您尚未安装,您可以从我们网站上的相应页面获取 rustup,并查看 GitHub 上 1.51.0 的详细发布说明

1.51.0 稳定版的新特性

此版本代表了 Rust 语言和 Cargo 近期最大的新增功能之一,稳定了一个常量泛型 MVP 和 Cargo 的新特性解析器。让我们直接深入了解!

常量泛型 MVP

在此版本之前,Rust 允许您的类型通过生命周期或类型进行参数化。例如,如果我们想要有一个泛型结构体,该结构体在其数组的元素类型上是泛型的,我们可以编写如下代码

struct FixedArray<T> {
              // ^^^ Type generic definition
    list: [T; 32]
        // ^ Where we're using it.
}

如果我们随后使用 FixedArray<u8>,编译器将创建一个 FixedArray 的单态版本,如下所示

struct FixedArray<u8> {
    list: [u8; 32]
}

这是一个强大的功能,允许您编写可重用的代码,而没有运行时开销。但是,直到此版本发布,还无法轻松地在这些类型的上进行泛型化。这在数组中最为明显,数组在其类型定义中包含其长度 ([T; N]),而以前您无法对其进行泛型化。现在,在 1.51.0 中,您可以编写对任何整数、boolchar 类型的值进行泛型化的代码!(使用 structenum 值仍然不稳定。)

此更改现在允许我们拥有自己的数组结构体,该结构体在其类型长度上都是泛型的。让我们看一个示例定义,以及如何使用它。

struct Array<T, const LENGTH: usize> {
    //          ^^^^^^^^^^^^^^^^^^^ Const generic definition.
    list: [T; LENGTH]
    //        ^^^^^^ We use it here.
}

现在,如果我们随后使用 Array<u8, 32>,编译器将创建一个 Array 的单态版本,如下所示

struct Array<u8, 32> {
    list: [u8; 32]
}

常量泛型为库设计者添加了一个重要的新工具,用于创建新的、强大的编译时安全 API。如果您想了解有关常量泛型的更多信息,还可以查看“常量泛型 MVP 进入 Beta 版”博客文章,以获取有关该功能及其当前限制的更多信息。我们迫不及待地想看看您创建的新库和 API!

array::IntoIter 稳定化

作为常量泛型稳定化的一部分,我们还稳定了一个使用它的新 API,std::array::IntoIterIntoIter 允许您在任何数组上创建按值迭代器。以前,没有方便的方法来迭代数组的拥有值,只能迭代它们的引用。

fn main() {
  let array = [1, 2, 3, 4, 5];
  
  // Previously
  for item in array.iter().copied() {
      println!("{}", item);
  }
  
  // Now
  for item in std::array::IntoIter::new(array) {
      println!("{}", item);
  }
}

请注意,这是作为单独的方法添加的,而不是数组上的 .into_iter(),因为目前这会引入一定程度的破坏性更改;目前 .into_iter() 指的是切片的按引用迭代器。我们正在探索在未来使其更符合人体工程学的方法。

Cargo 的新特性解析器

依赖管理是一个难题,其中最难的部分之一是选择在两个不同的包都依赖于同一个依赖项时,应该使用哪个版本的依赖项。这不仅包括其版本号,还包括为该包启用或禁用了哪些特性。Cargo 的默认行为是在单个包多次出现在依赖关系图中时,合并该包的特性。

例如,假设您有一个名为 foo 的依赖项,其中包含特性 A 和 B,它被包 barbaz 使用,但 bar 依赖于 foo+A,而 baz 依赖于 foo+B。Cargo 将合并这两个特性,并将 foo 编译为 foo+AB。这样做的好处是您只需编译 foo 一次,然后就可以将其重用于 barbaz

但是,这也存在一个缺点。如果在构建依赖项中启用的特性与您要构建的目标不兼容怎么办?

生态系统中一个常见的例子是许多 #![no_std] crate 中包含的可选 std 特性,该特性允许 crate 在 std 可用时提供附加功能。现在假设您想在您的 #![no_std] 二进制文件中使用 foo#![no_std] 版本,并在您的 build.rs 中的构建时使用 foo。如果您的构建时依赖项依赖于 foo+std,则您的二进制文件现在也依赖于 foo+std,这意味着它将不再编译,因为 std 不适用于您的目标平台。

长期以来,这一直是 cargo 中的一个问题,在此版本中,您的 Cargo.toml 中有一个新的 resolver 选项,您可以在其中设置 resolver="2",以告知 cargo 尝试一种新的特性解析方法。您可以查看 RFC 2957 以获得对该行为的详细描述,该行为可以总结如下。

  • 开发依赖项 — 当一个包同时作为普通依赖项和开发依赖项共享时,仅当当前构建包含开发依赖项时,才会启用开发依赖项特性。
  • 宿主依赖项 — 当一个包同时作为普通依赖项以及构建依赖项或过程宏共享时,普通依赖项的特性与构建依赖项或过程宏的特性保持独立。
  • 目标依赖项 — 当一个包在构建图中多次出现,并且其中一个实例是特定于目标的依赖项时,则仅当当前正在构建目标时,才会启用特定于目标的依赖项的特性。

虽然这可能会导致某些 crate 被多次编译,但这应该在使用带有 cargo 的特性时提供更直观的开发体验。如果您想了解更多信息,还可以阅读 Cargo Book 中的“特性解析器”部分以获取更多信息。我们要感谢 cargo 团队和所有参与设计和实施新解析器的人员的辛勤工作!

[package]
resolver = "2"
# Or if you're using a workspace
[workspace]
resolver = "2"

拆分调试信息

虽然在发布说明中不常强调,但 Rust 团队一直在努力改进 Rust 的编译时间,而此版本标志着 Rust 在 macOS 上编译时间方面长期以来最大的改进之一。调试信息将二进制代码映射回您的源代码,以便程序可以在运行时为您提供有关出错原因的更多信息。在 macOS 中,调试信息以前是使用名为 dsymutil 的工具收集到一个 .dSYM 文件夹中,这可能需要一些时间并占用相当多的磁盘空间。

将所有调试信息收集到此目录中有助于在运行时查找它,尤其是在二进制文件被移动的情况下。但是,它确实有一个缺点,即使您对程序进行了小的更改,dsymutil 也需要对整个最终二进制文件运行,以生成最终的 .dSYM 文件夹。这有时会大大增加构建时间,尤其是对于较大的项目,因为所有依赖项始终会被重新收集,但这一直是必要的步骤,因为如果没有它,Rust 的标准库就不知道如何在 macOS 上加载调试信息。

最近,Rust 回溯已切换到使用不同的后端,该后端支持加载调试信息而无需运行 dsymutil,并且我们已稳定了对跳过 dsymutil 运行的支持。这可以显着加快包含调试信息的构建速度,并显着减少使用的磁盘空间量。我们尚未运行广泛的基准测试,但已看到许多报告称,人们在 macOS 上使用此行为构建的速度要快得多。

您可以通过在运行 rustc 时设置 -Csplit-debuginfo=unpacked 标志,或通过在 Cargo 中将 split-debuginfo [profile] 选项设置为 unpacked 来启用此新行为。“unpacked”选项指示 rustc 将 .o 对象文件保留在构建输出目录中,而不是删除它们,并跳过运行 dsymutil 的步骤。Rust 的回溯支持足够智能,知道如何查找这些 .o 文件。lldb 等工具也知道如何执行此操作。只要您不需要在保留调试信息的同时将二进制文件移动到不同的位置,这应该就可以正常工作。

[profile.dev]
split-debuginfo = "unpacked"

稳定的 API

总而言之,此版本稳定了 18 个用于各种类型(如 slicePeekable)的新方法。一个值得注意的补充是 ptr::addr_of!ptr::addr_of_mut! 的稳定化,它们允许您创建指向未对齐字段的原始指针。以前这是不可能的,因为 Rust 要求 &/&mut 对齐并指向已初始化的数据,而 &addr as *const _ 随后会导致未定义行为,因为 &addr 需要对齐。这两个宏现在允许您安全地创建未对齐的指针。

use std::ptr;

#[repr(packed)]
struct Packed {
    f1: u8,
    f2: u16,
}

let packed = Packed { f1: 1, f2: 2 };
// `&packed.f2` would create an unaligned reference, and thus be Undefined Behavior!
let raw_f2 = ptr::addr_of!(packed.f2);
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);

以下方法已稳定。

其他更改

Rust 1.51.0 版本中还有其他更改:查看 RustCargoClippy 中的更改。

1.51.0 的贡献者

许多人齐心协力创建了 Rust 1.51.0。没有你们所有人,我们不可能做到。谢谢!