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
,它对 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
来获得对选项内部的引用,但我们除了获得引用之外什么也做不了,因为我们不能从 &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 中,你现在可以声明返回 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,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。如果没有你们所有人,我们不可能做到这一点。