Rust 1.26 发布

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

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

如果您之前通过 rustup 安装了 Rust,则获取 Rust 1.26.0 非常简单,只需执行以下命令:

$ rustup update stable

如果您还没有安装 rustup,您可以从我们网站的相应页面获取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 才能获得对 option 内部的引用,但我们只能获得一个引用,因为我们无法从 &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

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

main 可以返回 Result

说到痛点,由于 Rust 使用 Result 类型返回错误,并使用 ? 使处理它们变得容易,因此新 Rustacean 的一个常见痛点是尝试在 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

没错,由于 iu8,所以这会溢出,与编写 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: 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。没有你们所有人,我们不可能做到这一点。

感谢!