宣布 Rust 1.26

2018 年 5 月 10 日 · Rust 核心团队

Rust 团队很高兴宣布 Rust 的新版本 1.26.0。Rust 是一种系统编程语言,专注于安全、速度和并发。

如果您之前通过 rustup 安装了 Rust,获取 Rust 1.26.0 就像

$ rustup update stable

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

1.26.0 稳定版中的新功能

过去几个版本一直在稳定地添加一些相对较小的功能。然而,我们一直在努力开发很多东西,这些东西现在都开始在稳定版中发布。1.26 可能是自 Rust 1.0 以来功能最丰富的版本。让我们深入了解一下!

"Rust 编程语言" 第二版

在近 18 个月的时间里,Carol、Steve 和其他人一直在努力完成 "Rust 编程语言" 的全面重写。自从第一本书出版以来,我们了解了很多关于人们如何学习 Rust 的知识,这个版本在各个方面都得到了改进。

我们已经将第二版的草稿发布到网站上有一段时间了,但附带了一个免责声明,说明它还在进行中。目前,这本书正在进行一些最后的、微小的校对,并准备印刷。因此,随着这个版本的发布,我们建议使用第二版而不是第一版。您可以在 doc.rust-lang.org 上阅读它,或者通过 rustup doc --book 在本地阅读它。

说到印刷,您可以从 NoStarch Press预订这本书的纸质版。内容相同,但您会得到一本漂亮的实体书放在书架上,或者一个排版精美的 PDF 文件。收益将捐赠给慈善机构。

impl Trait

impl Trait 终于来了!这个功能已经被人期待了很久,它提供了一个被称为 "存在类型" 的功能。然而,它比听起来简单。它的核心是这个想法

fn foo() -> impl Trait {
    // ...
}

这个类型签名表示 "foo 是一个不接受参数但返回一个实现 Trait 特性的类型的函数"。也就是说,我们不会告诉你 foo 的实际返回类型是什么,只会告诉你它实现了一个特定的特性。你可能会想知道这与特性对象有什么区别

fn foo() -> Box<Trait> {
    // ...
}

虽然你今天可以写这段代码,但在所有情况下它都不是理想的。假设我们有一个特性 Trait,它对 i32f32 都实现了

trait Trait {
    fn method(&self);
}

impl Trait for i32 {
    // implementation goes here
}

impl Trait for f32 {
    // implementation goes here
}

考虑这个函数

fn foo() -> ? {
    5
}

我们想用一些东西来填充返回类型。以前,只有特性对象版本是可能的

fn foo() -> Box<Trait> {
    Box::new(5) as Box<Trait>
}

但这引入了 Box,这意味着分配。我们实际上也没有返回任何动态数据,因此特性对象的动态分派也会造成损害。因此,从 Rust 1.26 开始,你可以这样写

fn foo() -> impl Trait {
    5
}

这不会创建一个特性对象,它就像我们写了 -> i32 一样,但我们只提到了关于 Trait 的部分。我们得到了静态分派,但我们可以像这样隐藏真正的类型。

为什么这有用?一个很好的用途是闭包。请记住,Rust 中的闭包都有一个唯一的、不可写的类型,但它们实现了 Fn 特性。这意味着,如果你的函数返回一个闭包,你可以这样做

// before
fn foo() -> Box<Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

// after
fn foo() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

没有装箱,没有动态分派。当返回迭代器时,会发生类似的情况。迭代器不仅经常包含闭包,而且由于它们嵌套,你得到了非常深层的嵌套类型。例如

fn foo() {
    vec![1, 2, 3]
        .into_iter()
        .map(|x| x + 1)
        .filter(|x| x % 2 == 0)
}

编译时,会给出以下错误

error[E0308]: mismatched types
 --> src/main.rs:5:5
  |
5 | /     vec![1, 2, 3]
6 | |         .into_iter()
7 | |         .map(|x| x + 1)
8 | |         .filter(|x| x % 2 == 0)
  | |_______________________________^ expected (), found struct `std::iter::Filter`
  |
  = note: expected type `()`
             found type `std::iter::Filter<std::iter::Map<std::vec::IntoIter<{integer}>, [closure@src/main.rs:7:14: 7:23]>, [closure@src/main.rs:8:17: 8:31]>`

这是一个巨大的 "发现类型"。链中的每个适配器都会添加一个新的类型。此外,我们还有那个闭包。以前,我们必须在这里使用特性对象,但现在我们可以简单地做

fn foo() -> impl Iterator<Item = i32> {
    vec![1, 2, 3]
        .into_iter()
        .map(|x| x + 1)
        .filter(|x| x % 2 == 0)
}

就完成了。使用futures 非常类似。

需要注意的是,有时特性对象仍然是你需要的。只有当你的函数返回单个类型时,你才能使用 impl Trait;如果你想返回多个类型,你需要动态分派。例如

fn foo(x: i32) -> Box<Iterator<Item = i32>> {
    let iter = vec![1, 2, 3]
        .into_iter()
        .map(|x| x + 1);

    if x % 2 == 0 {
        Box::new(iter.filter(|x| x % 2 == 0))
    } else {
        Box::new(iter)
    }
}

在这里,我们可能会返回一个过滤后的迭代器,也可能不会。有两个不同的类型可以返回,因此我们必须使用特性对象。

哦,还有一件事:为了使语法更对称,你也可以在参数位置使用 impl Trait。也就是说

// before
fn foo<T: Trait>(x: T) {

// after
fn foo(x: impl Trait) {

对于短签名来说,这看起来可能更漂亮。

对于那些类型理论家来说,这是一个旁注:这不是一个存在量词,而是一个全称量词。换句话说,impl Trait 在输入位置是全称量词,但在输出位置是存在量词。

更漂亮的 match 绑定

你有没有遇到过对 Option 的引用,并尝试使用 match?例如,像这样的代码

fn hello(arg: &Option<String>) {
    match arg {
        Some(name) => println!("Hello {}!", name),
        None => println!("I don't know who you are."),
    }
}

如果你尝试在 Rust 1.25 中编译这段代码,你会得到以下错误

error[E0658]: non-reference pattern used to match a reference (see issue #42640)
 --> src/main.rs:6:9
  |
6 |         Some(name) => println!("Hello {}!", name),
  |         ^^^^^^^^^^ help: consider using a reference: `&Some(name)`

error[E0658]: non-reference pattern used to match a reference (see issue #42640)
 --> src/main.rs:7:9
  |
7 |         None => println!("I don't know who you are."),
  |         ^^^^ help: consider using a reference: `&None`

好吧,当然。让我们修改一下代码

fn hello(arg: &Option<String>) {
    match arg {
        &Some(name) => println!("Hello {}!", name),
        &None => println!("I don't know who you are."),
    }
}

我们添加了编译器抱怨的 &。让我们再试着编译一下

error[E0507]: cannot move out of borrowed content
 --> src/main.rs:6:9
  |
6 |         &Some(name) => println!("Hello {}!", name),
  |         ^^^^^^----^
  |         |     |
  |         |     hint: to prevent move, use `ref name` or `ref mut name`
  |         cannot move out of borrowed content

好吧,当然。让我们通过接受它的建议来让编译器再次满意

fn hello(arg: &Option<String>) {
    match arg {
        &Some(ref name) => println!("Hello {}!", name),
        &None => println!("I don't know who you are."),
    }
}

这将最终编译。我们必须添加两个 & 和一个 ref。但更重要的是,这些对我们作为程序员来说并没有什么帮助。当然,我们一开始忘记了一个 &,但这重要吗?我们必须添加 ref 来获得对选项内部的引用,但我们除了获得引用之外什么也做不了,因为我们不能从 &T 中移动出来。

因此,从 Rust 1.26 开始,最初的代码(没有 &ref)将直接编译,并完全按照你的预期执行。简而言之,编译器将在 match 语句中自动引用或取消引用。因此,当我们说

    match arg {
        Some(name) => println!("Hello {}!", name),

编译器会自动引用 Some,并且由于我们正在借用,name 也将自动绑定为 ref name。如果我们正在修改

fn hello(arg: &mut Option<String>) {
    match arg {
        Some(name) => name.push_str(", world"),
        None => (),
    }
}

编译器将自动通过可变引用借用,name 也将绑定为 ref mut

我们认为这将消除新老 Rustaceans 的一个重大痛点。编译器将更频繁地做正确的事情,而无需使用样板代码。

main 可以返回 Result

说到痛点,由于 Rust 使用 Result 类型来返回错误,并使用 ? 来简化处理,新 Rustaceans 的一个常见痛点是尝试在 main 中使用 ?

use std::fs::File;

fn main() {
    let f = File::open("bar.txt")?;
}

这将给出类似 "error[E0277]: ? 运算符只能在返回 Result 的函数中使用" 的错误。这导致了许多人编写代码,这些代码看起来像这样

fn run(config: Config) -> Result<(), Box<Error>> {
    // --snip--
}

fn main() {
    // --snip--

    if let Err(e) = run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

我们的 run 函数包含所有真正的逻辑,main 调用 run,只检查是否有错误并退出。我们需要创建这个第二个函数,因为 main 不能返回 Result,但我们想在该逻辑中使用 ?

在 Rust 1.26 中,你现在可以声明返回 Resultmain

use std::fs::File;

fn main() -> Result<(), std::io::Error> {
    let f = File::open("bar.txt")?;

    Ok(())
}

现在这可以正常工作了!如果 main 返回错误,这将以错误代码退出,并打印出错误的调试表示形式。

使用 ..= 的包含范围

早在 Rust 1.0 之前,你就可以使用 .. 创建排他范围,就像这样

for i in 1..3 {
    println!("i: {}", i);
}

这将打印 i: 1,然后打印 i: 2。在 Rust 1.26 中,你现在可以创建包含范围,就像这样

for i in 1..=3 {
    println!("i: {}", i);
}

这将像以前一样打印 i: 1,然后打印 i: 2,但也会打印 i: 3;3 包含在范围内。包含范围在你想遍历范围内所有可能的值时特别有用。例如,这是一个令人惊讶的 Rust 程序

fn takes_u8(x: u8) {
    // ...
}

fn main() {
    for i in 0..256 {
        println!("i: {}", i);
        takes_u8(i);
    }
}

这个程序做什么?答案:什么也不做。我们在编译时得到的警告有一个提示

warning: literal out of range for u8
 --> src/main.rs:6:17
  |
6 |     for i in 0..256 {
  |                 ^^^
  |
  = note: #[warn(overflowing_literals)] on by default

没错,由于 i 是一个 u8,这会导致溢出,等同于写 for i in 0..0,因此循环执行零次。

但是,我们可以使用包含范围来做到这一点

fn takes_u8(x: u8) {
    // ...
}

fn main() {
    for i in 0..=255 {
        println!("i: {}", i);
        takes_u8(i);
    }
}

这将产生你可能期望的 256 行输出。

基本切片模式

另一个期待已久的功能是 "切片模式"。这些模式允许你像匹配其他数据类型一样匹配切片。例如

let arr = [1, 2, 3];

match arr {
    [1, _, _] => "starts with one",
    [a, b, c] => "starts with something else",
}

在这种情况下,我们知道 arr 的长度为 3,因此我们需要在 [] 中包含三个条目。我们也可以在不知道长度的情况下匹配

fn foo(s: &[u8]) {
    match s {
        [a, b] => (),
        [a, b, c] => (),
        _ => (),
    }
}

在这里,我们不知道 s 的长度,因此我们可以写出前两个分支,每个分支都有不同的长度。这也意味着我们需要一个 _ 项,因为我们没有覆盖所有可能的长度,我们也无法覆盖!

速度改进

我们继续努力提高编译器的速度。我们发现,在某些情况下,深度嵌套类型是非线性的,并且实施了一个修复程序。我们看到,由于这个更改,编译时间减少了 12%,但许多其他较小的修复程序也已发布。未来还会有更多改进!

128 位整数

最后,一个非常简单的功能:Rust 现在有了 128 位整数!

let x: i128 = 0;
let y: u128 = 0;

这些是 u64 大小的两倍,因此可以容纳更多值。更具体地说,

  • u128: 0 - 340,282,366,920,938,463,463,374,607,431,768,211,455
  • i128: −170,141,183,460,469,231,731,687,303,715,884,105,728 - 170,141,183,460,469,231,731,687,303,715,884,105,727

哇!

查看 详细的发布说明了解更多信息。

库稳定化

我们稳定了 fs::read_to_string,它比 File::openio::Read::read_to_string 更方便,可以轻松地将整个文件一次性读入内存。

use std::fs;
use std::net::SocketAddr;

let foo: SocketAddr = fs::read_to_string("address.txt")?.parse()?;

您现在可以使用 Debug 格式将数字格式化为十六进制

assert!(format!("{:02x?}", b"Foo\0") == "[46, 6f, 6f, 00]")

尾随逗号 现在受标准库中所有宏的支持

查看 详细的发布说明了解更多信息。

Cargo 功能

Cargo 在此版本中没有收到很多大的新功能,而是看到了稳定性和性能改进的稳定流。Cargo 现在应该能够更快地解析锁定文件,更智能地回溯,并且减少对手动 cargo update 调用的需求。Cargo 的二进制文件 现在也与 rustc 共享相同的版本

查看 详细的发布说明了解更多信息。

1.26.0 的贡献者

许多人共同创建了 Rust 1.26。如果没有你们所有人,我们不可能做到这一点。

谢谢!