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
,它同时为 i32
和 f32
实现:
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 中,您现在可以声明返回 Result
的 main
:
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
: 0 - 340,282,366,920,938,463,463,374,607,431,768,211,455i128
:−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::open
和 io::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。没有你们所有人,我们不可能做到这一点。