这篇文章是与 Rust 团队(文章中的“我们”)合作撰写的。你也可以在 Rust 博客上阅读他们的公告。
从今天开始,Rust 2018 版本正式发布。在这次版本更新中,我们专注于提高开发效率… 帮助 Rust 开发者发挥最大的生产力。
但除此之外,很难准确地解释 Rust 2018 是什么。
有些人认为它是一个新版本的语言,它确实算… 算是一个新版本,但也并非完全如此。我说“并非完全如此”是因为如果它是一个新版本,它不会像其他语言中的版本控制那样运作。
在大多数其他语言中,当一个新版本的语言发布时,任何新功能都会添加到那个新版本中。旧版本不会获得新功能。
Rust 版本不同。这是因为语言的演进方式。几乎所有新功能都与现有的 Rust 语言完全兼容。它们不需要任何重大变更。这意味着没有理由将它们限制在 Rust 2018 代码中。新版本的编译器将继续支持“Rust 2015 模式”,这是你默认获得的模式。
但有时为了推进语言发展,你需要添加一些东西,比如新的语法。而这种新的语法可能会破坏现有代码库中的某些东西。
一个例子是 async/await
功能。Rust 最初没有 async
和 await
的概念。但事实证明,这些原语非常有用。它们使编写异步代码变得更容易,而不会让代码变得笨拙。
为了实现这个功能,我们需要添加 async
和 await
作为关键字。但我们也必须小心,不能使旧代码失效… 那些可能将 async
或 await
作为变量名的代码。
所以我们将这些关键字作为 Rust 2018 的一部分添加。即使该功能还没有实现,这些关键字现在已经被保留了。未来三年开发所需的所有重大变更(如添加新关键字)将一次性在 Rust 1.31 中完成。
即使 Rust 2018 中有重大变更,但这并不意味着你的代码会崩溃。即使你的代码中有 async
或 await
作为变量名,它仍然可以编译。除非你另行指示,否则编译器会假设你想以与之前相同的方式编译你的代码。
但只要你想使用这些新的、有重大变更的功能,你就可以选择加入 Rust 2018 模式。你只需运行 cargo fix
,它会告诉你是否需要更新代码来使用新功能。它也会在很大程度上自动化更改过程。然后你可以在你的 Cargo.toml 中添加 edition=2018
来选择加入并使用新功能。
Cargo.toml 中的这个版本说明符不会应用于整个项目… 它不会应用于你的依赖项。它只应用于某个特定的 crate。这意味着你可以在 crate 图中混合使用 Rust 2015 和 Rust 2018。
正因为如此,即使 Rust 2018 发布了,它在大多数情况下看起来也会与 Rust 2015 相似。大多数更改都会同时出现在 Rust 2018 和 Rust 2015 中。只有少数需要重大变更的功能才会被排除在外。
不过,Rust 2018 不仅仅是关于核心语言的变更。事实上,远不止于此。
Rust 2018 旨在提高 Rust 开发者的生产力。许多提高生产力的成果来自核心语言之外的东西… 例如工具。它们也来自专注于特定用例,并找出 Rust 如何成为这些用例中最有效的语言。
所以你可以把 Rust 2018 当作 Cargo.toml 中的说明符,它用来启用少数需要重大变更的功能…
或者你可以把它看作一个时间点,Rust 在许多情况下成为你能够使用的最有效率的语言之一——无论何时你需要性能、轻量级占用空间或高可靠性。
在我们看来,它是后者。因此,让我们看看核心语言之外的所有变化。然后我们可以深入探讨核心语言本身。
Rust 用于特定用例
编程语言本身,在抽象意义上无法产生效益。它只有在被应用于某项用途时才能产生效益。正因为如此,团队知道我们不仅仅需要改进 Rust 语言本身或 Rust 工具。我们还需要简化 Rust 在特定领域的应用。
在某些情况下,这意味着为一个全新的生态系统创建一整套新的工具。
在其他情况下,这意味着完善已经存在的生态系统,并对其进行良好记录,以便轻松上手。
Rust 团队组成了专注于四个领域的专门小组
- WebAssembly
- 嵌入式应用
- 网络
- 命令行工具
WebAssembly
对于 WebAssembly,专门小组需要创建一整套全新的工具。
就在去年,WebAssembly 实现了将 Rust 等语言编译为在网页上运行。从那时起,Rust 迅速成为与现有 Web 应用程序进行集成的最佳语言。
Rust 非常适合 Web 开发,有两个原因
- Cargo 的 crate 生态系统与大多数 Web 应用开发人员习惯使用的生态系统相同。你将许多小的模块整合在一起形成一个更大的应用程序。这意味着可以轻松地在需要的地方使用 Rust。
- Rust 的占地面积很小,不需要运行时。这意味着你不需要下载大量代码。如果你有一个很小的模块执行大量的繁重计算工作,你只需引入几行 Rust 代码就能使其运行得更快。
有了web-sys
和 js-sys
crate,你可以轻松地从 Rust 代码中调用 Web API,例如 fetch
或 appendChild
。而 wasm-bindgen
使得轻松支持 WebAssembly 本身不支持的高级数据类型。
当你编写好你的 Rust WebAssembly 模块后,就会有一些工具可以轻松地将其插入你的 Web 应用程序的其余部分。你可以使用wasm-pack自动运行这些工具,并根据需要将新模块推送到 npm。
查看Rust 和 WebAssembly 书籍,自己尝试一下。
接下来是什么?
现在 Rust 2018 已经发布了,专门小组正在考虑下一步的行动方向。他们将与社区合作,确定未来的重点领域。
嵌入式
对于嵌入式开发,专门小组需要使现有的功能稳定。
从理论上讲,Rust 一直都是嵌入式开发的良好语言。它为嵌入式开发者提供了他们急需的现代工具,以及非常方便的高级语言功能。所有这些都无需牺牲资源使用率。因此,Rust 看起来非常适合嵌入式开发。
然而,在实际应用中,它有点像是疯狂的旅程。必要的特性不在稳定通道中。此外,标准库需要针对嵌入式设备进行调整。这意味着人们必须编译自己的 Rust 核心 crate 版本(这个 crate 用于每个 Rust 应用程序,以提供 Rust 的基本构建块——内联函数和原语)。
这两点加起来意味着开发者必须依赖 Rust 的 nightly 版本。由于微控制器目标没有自动测试,nightly 版本经常会针对这些目标出现问题。
为了解决这个问题,专门小组需要确保必要的特性在稳定通道中。我们还必须在 CI 系统中添加针对微控制器目标的测试。这意味着一个人添加桌面组件的东西不会影响嵌入式组件。
有了这些变化,使用 Rust 进行嵌入式开发从前沿技术走向了生产力的高峰。
查看嵌入式 Rust 书籍,自己尝试一下。
接下来是什么?
通过今年的努力,Rust 对 ARM Cortex-M 系列微处理器内核提供了非常好的支持,这些内核广泛应用于各种设备。然而,嵌入式设备中使用了许多架构,而这些架构的支持力度不足。Rust 需要扩展以对这些其他架构提供相同级别的支持。
网络
对于网络,专门小组需要在语言中构建一个核心抽象——async/await
。这样,即使代码是异步的,开发者也可以使用惯用的 Rust 代码。
在网络任务中,你经常需要等待。例如,你可能正在等待对请求的响应。如果你的代码是同步的,这意味着工作将停止——运行代码的 CPU 内核无法做任何其他事情,直到请求进来。但是,如果你异步编码,那么等待响应的函数可以保持挂起状态,而 CPU 内核则负责运行其他函数。
即使使用 Rust 2015,也可以使用异步 Rust 编码。这有很多好处。从大的方面来说,对于像服务器应用程序这样的东西,这意味着你的代码可以处理每个服务器更多的连接。从小的方面来说,对于像在微型单线程 CPU 上运行的嵌入式应用程序这样的东西,这意味着你可以更好地利用你的单线程。
但是这些好处也带来了一个主要的缺点——你无法对该代码使用借用检查器,而且你必须编写非惯用的(而且有点混乱的)Rust 代码。这就是 async/await
的用武之地。它为编译器提供了它跨异步函数调用进行借用检查所需的信息。
async/await
的关键字是在 1.31 版本中引入的,尽管它们目前没有得到实现的支持。大部分工作已经完成,你预计该功能将在即将发布的版本中提供。
接下来是什么?
除了为网络应用程序启用高效的低级开发外,Rust 还可以启用更高层次的更高效开发。
许多服务器需要执行相同类型的任务。它们需要解析 URL 或使用 HTTP。如果将这些内容转变为组件——可以作为板条箱共享的通用抽象——那么将它们组合起来以形成各种不同的服务器和框架将变得容易。
为了推动组件开发过程,Tide 框架 提供了一个测试平台,最终将用于这些组件的示例用法。
命令行工具
对于命令行工具,工作组需要将更小的低级库组合到更高层次的抽象中,并完善一些现有的工具。
对于一些 CLI 脚本,你确实想使用 bash。例如,如果你只需要调用其他 shell 工具并在它们之间传递数据,那么 bash 是最好的。
但是 Rust 非常适合其他类型的 CLI 工具。例如,如果你正在构建一个像 ripgrep 这样的复杂工具,或者正在构建一个基于现有库功能的 CLI 工具,那么它非常棒。
Rust 不需要运行时,并且允许你编译成一个单独的静态二进制文件,这使得它易于分发。而且你获得了其他语言(如 C 和 C++)没有的高级抽象,这使得 Rust CLI 开发人员已经可以提高效率。
工作组需要做些什么才能使它变得更好?甚至更高层次的抽象。
使用这些更高层次的抽象,你可以快速轻松地组装一个可用于生产的 CLI。
其中一个抽象的例子是 human panic 库。如果没有这个库,如果你的 CLI 代码出现恐慌,它可能会输出整个回溯。但这对你的最终用户来说没有多大帮助。你可以添加自定义错误处理,但这需要付出努力。
如果你使用 human panic,那么输出将自动路由到错误转储文件。用户将看到一条有用的消息,建议他们报告问题并上传错误转储文件。
工作组还简化了 CLI 开发的入门步骤。例如,confy 库将自动执行新 CLI 工具的大量设置。它只询问你两件事
- 你的应用程序名称是什么?
- 你想公开哪些配置选项(你将其定义为可以序列化和反序列化的结构体)?
有了这些,confy 将为你完成剩下的工作。
接下来是什么?
工作组抽象了许多 CLI 之间常见的不同任务。但是,仍然可以抽象出更多内容。工作组将创建更多此类高级库,并在开发过程中修复更多错误。
Rust 工具
当你体验一种语言时,你通过工具体验它。这从你使用的编辑器开始。它贯穿开发过程的每个阶段,以及维护过程。
这意味着一种高效的语言依赖于高效的工具。
以下是一些工具(以及对 Rust 现有工具的改进),这些工具作为 Rust 2018 的一部分被引入。
IDE 支持
当然,效率取决于你快速流畅地将代码从你的脑海中转移到屏幕上。IDE 支持对此至关重要。为了支持 IDE,我们需要能够告诉 IDE Rust 代码的实际含义的工具——例如,告诉 IDE 代码完成时哪些字符串是合理的。
在 Rust 2018 推出过程中,社区专注于 IDE 所需的功能。借助 Rust Language Server 和 IntelliJ Rust,许多 IDE 现在都拥有流畅的 Rust 支持。
更快的编译
对于编译来说,更快意味着更高效。因此,我们让编译器更快了。
以前,当你编译一个 Rust 板条箱时,编译器会重新编译板条箱中的每个文件。但是现在,有了 增量编译,编译器变得智能,只重新编译已更改的部分。这与 其他优化 相结合,使 Rust 编译器快得多。
rustfmt
效率还意味着不必修复样式错误(并且永远不必就格式化规则争论)。
rustfmt 工具通过使用默认代码样式自动重新格式化代码来帮助解决这个问题(社区对此达成了一致)。使用 rustfmt 确保所有 Rust 代码都符合相同的样式,就像 clang 格式对 C++ 和 Prettier 对 JavaScript 所做的那样。
Clippy
有时,在你身边有一位经验丰富的顾问会很好……在你编码时给你一些最佳实践方面的建议。这就是 Clippy 的作用——它在你编码时查看你的代码,并告诉你如何使代码更惯用。
rustfix
但是,如果你有一个使用过时惯用的旧代码库,那么仅仅获得提示并自己更正代码可能很繁琐。你只想让某人进入你的代码库并进行更正。
针对这些情况,rustfix 将自动执行此过程。它将同时应用来自 Clippy 等工具的 lint,并将旧代码更新为匹配 Rust 2018 惯用方式。
对 Rust 本身的更改
生态系统中的这些变化带来了很多效率上的提升。但一些效率问题只能通过对语言本身的更改才能解决。
正如我在引言中提到的,大多数语言更改与现有的 Rust 代码完全兼容。这些更改都是 Rust 2018 的一部分。但由于它们不会破坏任何代码,因此它们在任何 Rust 代码中都有效……即使该代码没有使用 Rust 2018。
让我们看看添加到所有版本中的一些重要的语言特性。然后我们可以看看 Rust 2018 特定功能的简短列表。
所有版本的新的语言特性
以下是一些重要的新的语言特性示例,这些特性已存在(或将存在)于所有语言版本中。
更精确的借用检查(例如非词法生命周期)
Rust 的一大卖点是借用检查器。借用检查器有助于确保你的代码是内存安全的。但它也是新 Rust 开发人员的痛点。
部分原因是学习新概念。但还有一大部分……借用检查器有时会拒绝看起来应该可以工作的代码,即使是那些理解这些概念的人。
这是因为借用生命周期被认为一直持续到其作用域的末尾——例如,持续到包含变量的函数的末尾。
这意味着,即使该变量已完成使用该值,并且不会再尝试访问它,其他变量仍然会被拒绝访问该值,直到函数结束。
为了解决这个问题,我们让借用检查器变得更智能。现在它可以识别出变量何时真正完成使用一个值。如果它已经完成,那么它就不会阻止其他借用者使用数据。
虽然这目前仅在 Rust 2018 中可用,但它将在不久的将来在所有版本中可用。我很快会写更多关于这方面的内容。
稳定版 Rust 上的过程宏
Rust 中的宏在 Rust 1.0 之前就存在。但随着 Rust 2018,我们进行了一些重大改进,例如引入了过程宏。
使用过程宏,感觉就像你可以将自己的语法添加到 Rust 中。
Rust 2018 带来了两种过程宏
类函数宏
类函数宏允许你拥有看起来像普通函数调用,但实际上是在编译期间运行的东西。它们接受一些代码,并输出不同的代码,然后编译器将这些代码插入二进制文件。
它们已经存在一段时间了,但你可以使用它们做的事情很有限。你的宏只能接收输入代码,并对其运行匹配语句。它没有访问输入代码中所有标记的权限。
但是,使用过程宏,你可以获得与解析器相同的输入——标记流。这意味着你可以创建功能更强大的类函数宏。
属性类宏
如果你熟悉 JavaScript 等语言中的装饰器,属性宏非常类似。它们允许你用 Rust 中需要预处理并转换为其他内容的代码段进行注释。
derive
宏正是执行此类操作的。当你将 derive 放在结构体之上时,编译器将接收该结构体(在将其解析为标记列表之后)并对其进行修改。具体来说,它将在结构体中添加一个来自特征的基本函数实现。
匹配中的更符合人体工程学借用
此更改非常简单明了。
以前,如果你想借用某样东西并尝试对其进行匹配,你必须添加一些看起来很奇怪的语法
但现在,你不再需要 &Some(ref s)
。你只需写 Some(s)
,Rust 将从那里推断出。
Rust 2018 特定功能
Rust 2018 最小的部分是它特有的功能。以下是一些使用 Rust 2018 版本才能解锁的功能变化。
关键字
在 Rust 2018 中添加了一些关键字。
try
关键字async/await
关键字
这些功能尚未完全实现,但关键字将在 Rust 1.31 中添加。这意味着我们不必在这些关键字背后的功能实现之后引入新的关键字(这将是重大更改)。
模块系统
学习 Rust 的开发人员面临的一大痛点是模块系统。我们可以理解为什么。很难推断 Rust 将选择使用哪个模块。
为了解决这个问题,我们对 Rust 中路径的工作方式进行了一些更改。
例如,如果你导入了一个板条箱,你可以在顶层使用它。但如果你将任何代码移动到子模块,它将不再起作用。
// top level module extern crate serde; // this works fine at the top level impl serde::Serialize for MyType { ... } mod foo { // but it does *not* work in a sub-module impl serde::Serialize for OtherType { ... } }
另一个例子是前缀 ::
,它过去指的是板条箱根目录或外部板条箱。可能很难区分哪个是哪个。
我们现在将这一点更明确地说明。现在,如果您想引用包根目录,请使用前缀 `crate::`。这只是我们进行的路径清晰度改进之一。
如果您有现有的 Rust 代码,并且希望它使用 Rust 2018,您很可能需要针对这些新的模块路径更新它。但这并不意味着您需要手动更新您的代码。在您向 Cargo.toml 添加版本标识符之前,请运行 `cargo fix`,`rustfix` 将为您进行所有更改。
了解更多
在Rust 2018 版本指南中了解有关此版本的更多信息。
关于 Lin Clark
Lin 在 Mozilla 的高级开发部门工作,专注于 Rust 和 WebAssembly。
12 条评论