稳定Rust的Never类型
摘要
Rust在经过两年多的开发后,稳定了其'never'类型,这一特性使得泛型代码更高效,并简化了语言中的类型推断。
暂无内容
查看缓存全文
缓存时间: 2026/09/12 20:32
# 稳定 Rust 的永不类型
来源:https://lwn.net/SubscriberLink/1091015/d9e48318ed242b41/
> ### 欢迎来到 LWN\.net
> 以下订阅专属内容由 LWN 订阅者提供给您。成千上万的订阅者依赖 LWN 获取来自 Linux 和自由软件社区的最新资讯。如果您喜欢这篇文章,请考虑订阅 LWN (https://lwn.net/subscribe/)。感谢您访问 LWN\.net!
函数的返回类型用于表示其产生的数据种类。Rust 的“永不”类型(以感叹号标记 (https://doc.rust-lang.org/stable/std/primitive.never.html)“\!”)是该语言用于标记永不返回的函数以及其他永远不可能出现值的场景的类型。长期以来,该类型仅在编译器内部使用,被视为不稳定的特性。8 月 24 日 (https://github.com/rust-lang/rust/pull/155499),经过两年多的努力,Rust 编译器贡献者“waffle”最终成功稳定了该类型。耗时如此之久的部分原因在于它涉及对先前 Rust 版本的小型破坏性更改,编译器维护者需要确保这些更改不会影响太多实际代码。
(注意:Rust 也使用感叹号表示宏调用。根据语法构造规则,适合使用永不类型的地方不是宏调用的有效位置,反之亦然。)
#### 为什么需要永不类型?
Rust 拥有永不类型有两个原因,一个实际,一个哲理。
实际原因在于它允许更高效的泛型代码。例如,考虑标准库中用于从字符串实例化类型的 `FromStr` (https://doc.rust-lang.org/std/str/trait.FromStr.html) 特征:
```rust
trait FromStr: Sized {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
```
`FromStr::from_str()` 要么返回转换结果,要么返回自定义错误类型。例如,尝试将 “foo” 转换为整数会返回一个 `ParseIntError` (https://doc.rust-lang.org/std/num/struct.ParseIntError.html)。但某些类型的转换永远不会失败。例如,将字符串转换为 `ByteString` (https://doc.rust-lang.org/std/bstr/struct.ByteString.html) 始终是可能的。`FromStr` 的该实现可以将 `Err` 设置为永不类型。这样编译器就能知道返回的 `Result` 的错误分支永远不会存在,从而可以优化掉所有处理或检查该分支的代码。
```rust
impl FromStr for ByteString {
type Err = !;
fn from_str(s: &str) -> Result<Self, Self::Err> { ... }
// 保持相同的泛型接口,
// 但生成的代码等效于:
// fn from_str(s: &str) -> Self { ... }
}
```
哲理原因涉及正确的类型推导。在 Rust 中,`if` 语句和 `while` 循环等结构是表达式;其结果可以赋值给变量。如果程序员编写一个无限循环,编译器需要为其结果推导类型。这在实际代码中并不常见,但事实证明,能够统一处理这种情况(而非添加特殊规则来处理)可以简化类型推导。特别是,永不类型具有一个简化代码的有用特性:它可以自动强制转换为任何其他类型。这听起来很奇怪,但它是安全的,因为永不类型代表永远无法产生值的“计算结果”。因此,任何声称拥有永不类型值的代码,编译器都知道不可能执行到那里,因此可以安全地忽略它。这是一种由类型系统驱动的死代码消除形式。
基于这两个原因,Rust 程序员一直希望在稳定版本的语言中使用永不类型。要实现这一点,需要解决一个特别棘手的边界情况。
#### 永不回退
由于永不类型到其他类型的转换实现方式,编译器有时可能陷入无法简单推导表达式具体类型的情况。考虑这个例子:它定义了一个永不返回的匿名函数(使用 `||`,类似于 Python 或 LISP 的 `lambda`),然后以期望具体错误类型的方式调用它(使用 `?` 运算符 (https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html?highlight=error#the--operator-shortcut)):
```rust
let function_that_never_returns = || { loop {} };
function_that_never_returns()?;
```
该无限循环被赋予类型 `!`,然后被隐式转换为函数应返回的类型。但由于函数是本地定义的,且未给出显式类型,编译器没有足够的信息来确定该类型是什么。可以通过给函数一个显式返回类型来解决这个问题:
```rust
let function_that_never_returns = || -> Foo { loop {} };
```
然而,由于这种注解仅在函数无法返回任何内容的情况下才需要,要求程序员为其指定一个虚构的类型就显得有些多此一举。因此,编译器包含一条特殊规则:如果在所有其他类型推导完成后,仍然存在无法确定的模糊类型,就假定它应是指定的回退类型。在 Rust 2024 版 (https://doc.rust-lang.org/edition-guide/editions/index.html) 之前,该回退类型是 `()`(单元类型,只有一个可能的值)。在 2024 版中,回退类型更改为 `!` 本身,这实质上取消了隐式转换。在编译器内部,永不类型仍然先被转换为未知类型然后再回退,但从程序员的角度来看,其行为等同于仅在类型需要合理时才进行隐式转换。这种行为变化在技术上属于破坏性更改。某些代码的类型推导可能会改变,进而可能导致编译错误。这正是 Rust 版本系统的用途:允许在前端设计中进行破坏性更改,而不会破坏旧代码或要求整个生态系统一次性更新。然而,此案例中,存在希望将新行为向后移植到旧版本的原因。
#### 永不不可失败
多年来,标准库一直有一个 `Infallible` (https://doc.rust-lang.org/std/convert/enum.Infallible.html) 类型,以应对永不类型的不稳定性。它在语义上与永不类型有相同的用途,但没有任何特殊的编译器支持。因此,使用它的代码在技术上是正确的但非最优(例如在枚举中有多余的标签层或发出死代码),因为优化器并非总能移除对 `Infallible` 的引用。原计划是当永不类型最终稳定时,`Infallible` 将成为 `!` 的类型别名,所有旧代码将无声地变得更高效。然而,人们指出重新定义 `Infallible` 在几种方式上意外地造成了破坏性更改。由于 `!` 具有隐式转换,更改 `Infallible` 的定义可能导致现有代码需要额外的类型说明符才能通过类型检查。幸运的是,更改 `Infallible` 的定义和更改默认回退类型,虽然都是破坏性更改,但几乎相互抵消。任何通过名称引用标准库 `Infallible` 类型的代码将继续工作;只有那些期望类型推导隐式产生 `Infallible` 的地方才有可能破坏现有代码。鉴于 Rust 在大多数情况下缺乏隐式转换,这通常发生在永不类型曾被隐式转换为 `Infallible` 的地方。如果将 `Infallible` 设为永不类型的别名,那么这些地方可能会发生永不类型回退,进而改变推导的类型,如果永不回退类型未同时更新,将导致编译错误。由于两项更改同时发生,Rust 维护者相信几乎所有现有 Rust 代码将继续编译——但“几乎所有”在处理向后不兼容更改时并不是一个令人放心的限定词。Rust 社区确实有一个解决方案,即 `crater` (https://github.com/rust-lang/crater#crater),它可以下载并编译来自 crates\.io (https://crates.io/) 的所有公开可用 Rust 库,以搜索因编译器更改而损坏的代码。
#### 永不言不
Waffle 于四月运行了 `crater`,并发现 (https://github.com/rust-lang/rust/pull/155499#issuecomment-4290467341),虽然有 3,300 个 crate 受到该更改的负面影响,但只有七个被完全破坏,其余的是因为依赖了已修复的旧版本库。在后一种情况下,理论上可以通过发布少数核心库的向后移植修复来解决问题。这并非偶然;自 2024 年以来,每当代码以会在新更改下破坏的方式触发永不类型回退时,Rust 一直在发出警告,因此大多数库有足够的时间主动更新。`crater` 观察到的最常见剩余错误是调用泛型函数时没有足够的类型信息供编译器选择特定返回类型。考虑这个函数:
```rust
fn foo<T: Default>() -> Result<T, SomeError> { ... }
```
它返回一个由调用者选择的、必须实现 `Default` 特征的类型 `T` 的值,或者一个错误。然而,如果在不指定 `T` 值的情况下调用它,那么类型回退可能会介入:
```rust
// 未指定类型。
foo()?;
```
以前,这会让编译器假定 `T` 应该是 `()`(实现了 `Default`),因此代码能编译。在此更改之后(并且在 2024 版上),编译器假定 `T` 应该是 `!`(未实现 `Default`),因此会导致编译错误。修复方法是在调用处或通过模式匹配赋值显式指定 `foo()` 应返回的类型。
```rust
foo::<()>()?;
// 或 () = foo()?;
```
尽管这不是一个复杂的更改,但 Rust 维护者不愿意破坏 3,300 个 crate。Waffle 被要求 (https://github.com/rust-lang/rust/pull/155499#issuecomment-4307446212) 与常见库的维护者合作,向后移植类似的简单更改(发布新的补丁版本 (https://semver.org/),许多 Rust 构建环境会自动获取),以减少依赖损坏依赖项的库数量。一些库作者愿意 (https://github.com/rust-lang/rust/pull/155499#issuecomment-4400482314) 进行向后移植,但一些人以这些旧版本已过生命期为由拒绝。这些维护者指出,用户可以继续使用旧版本的 Rust 或更新到库的受维护版本。即便如此,成功的向后移植解决了 1,553 个失败的 crate。在修复 (https://github.com/rust-lang/rust/issues/155924) 一些相关问题以进一步减少损坏的 crate 数量后,Rust 维护者最终达成共识:尽管仍会有一些代码损坏,但为了简化语言,进行此更改是值得的。
因此,从 Rust 1\.99 开始,永不类型将是稳定的,`Infallible` 将成为永不类型的类型别名。发现此更改破坏其代码的用户有几种选择:
- 保持使用 Rust 版本 1\.98。
- 将其依赖更新为包含该问题修复的受支持版本。
- 添加补丁以显式指定受影响函数调用的返回类型。
一方面,这是一个破坏性更改,人们可能会看到原本稳定且工作的代码突然无法编译。这可能被视为违反了 Rust 对向后兼容性的承诺。另一方面,这个问题相对罕见,有多种简单的修复方法,它已被警告多年,并且它一直是语言计划的一部分。此外,Rust 维护者直接与社区合作寻找和解决损坏问题,甚至帮助将修复向后移植到早已停止支持的流行库的旧版本。因此,整个过程也可以被视为对 Rust 向后兼容性承诺的肯定。
未来,学习这门语言的人希望会发现永不类型只是不那么特殊了一点。无论如何,大多数 Rust 用户可能完全不受影响,但永远别说“永不”。
相似文章
我稳定了never类型
经过十多年的不稳定和失败尝试,Rust中的never类型已经在nightly版本上稳定了,这标志着该编程语言的重大进展。
Rust 项目目标:不可移动类型与保证析构函数
Rust 项目目标概述了不可移动类型和保证析构函数的计划,以提升语言安全性和资源管理。
在 nightly 版本中启用下一代特征求解器 | Rust 博客
Rust 博客宣布在 nightly 版本中启用下一代特征求解器,这是一次重大编译器更新,替换核心类型系统组件,修复超过 200 个问题,并计划进行稳定化。
Rust 中的函数式状态机:Typestate 和 Newtype 模式
本文介绍了如何在 Rust 中使用 typestate 和 newtype 模式实现函数式状态机,以实现代码中的类型安全状态管理。
Rust:空类型并非底类型
这篇文章讨论了Rust中最近新增的空类型,解释了空类型和底类型之间的区别,以及它们如何影响语言中的类型强制转换。