Rust:空类型并非底类型
摘要
这篇文章讨论了Rust中最近新增的空类型,解释了空类型和底类型之间的区别,以及它们如何影响语言中的类型强制转换。
<p><a href="https://lobste.rs/s/lf3est/rust_when_empty_isn_t_bottom">评论</a></p>
查看缓存全文
缓存时间: 2026/09/08 23:09
# Rust:当空类型并非底部类型
来源:https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html
作者:ettolrach,2026年9月8日
本文使用JavaScript渲染数学公式和加载评论。除此之外我不使用它,因此如果您能为本页启用JavaScript,我将不胜感激 :)
*本篇博客文章未使用任何大语言模型撰写。*
Rust 最近为该语言添加了空类型(waffle, 2026)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:waffle_i_stabilized)。在 Rust 中,它被称为 `!` 或 `never`。虽然之前你可以将 `!` 用作函数返回类型,但不能用于其他类型注解。启用此功能以支持所有类型注解的拉取请求已于 2026 年 8 月 25 日合并,按撰写本文时的计划,将被添加到 Rust 1.100 中。
它的行为大致符合你的预期,但其中有一个有趣的细节让大多数了解它的人感到惊讶:类型为 `!` 的表达式并不一定能强制转换为任何其他类型。此外,空类型和底部类型实际上并不一定在任何语言中都是相同的,包括 Rust。
## 有何区别?
我们将空类型定义为没有值且没有构造器的类型。我们还有与之配套的函数:`fn absurd(x: !) -> A`。该函数接受任何类型为 `!` 的值,并返回你选择的类型的某个值(从技术上讲,它并不返回任何东西,因为你永远无法拥有类型为 `!` 的值来调用它,但能够构造‘任何东西’在概念上更有意义)。
底部类型也没有值且没有构造器。然而,它没有 `absurd` 函数,而是使用*子类型化*并使其成为所有类型的子类型。这意味着,得益于继承的工作原理,我们可以将其用作任何其他类型的替代(记住,如果类型 `A` 是类型 `B` 的子类型,那么只要代码需要 `B`,我们就可以使用 `A`)。
它们不同的原因应该是显而易见的:空类型不需要我们的语言具有子类型化,而底部类型需要!Rust 并未对所有类型具有子类型化(它对生命周期有效,但这与本题无关),因此它不可能拥有底部类型。但它们之间还有另一个更实际的区别。
## 两种类型在代码中如何工作
首先,我们将使用一个具有正式子类型化的语言:Scala。Scala 将空类型(由于它有子类型化,因此也是底部类型)称为 `Nothing`。为了利用 `Nothing`,我们将 `throw` 一个异常。尽管抛出的异常可以被捕获,但它不会像 Rust 中的 `panic!` 那样返回值给当前的局部代码。因此,它返回 `Nothing`。
```scala
val f: Int => Nothing = (_) => throw Error()
val g: Int => Int = f
@main def main() = ()
```
由于子类型化规则和一个称为*型变*的概念,我们不需要在这里做任何特殊处理,只需指定 `g` 使用 `f` 即可。
在 Rust 中,与 Scala 类似,我们将使用 `panic!` 来不返回任何值(从而返回类型 `!`)。由于 Rust 没有子类型化,我们需要使用 `absurd` 函数。Rust 会隐式地将任何类型为 `!` 的表达式包装一个 `absurd` 调用;实际上,Rust 并没有 `absurd` 函数(尽管我发现这样想很有帮助),它只是根据需要将类型为 `!` 的表达式隐式类型转换为任何类型。这种隐式类型转换在 Rust 中称为*强制转换*。
重要的是!如果类型在某处包含了 `!` 作为其他类型的一部分,例如函数类型,那么强制转换*不会*发生。这就是为什么以下代码无法通过类型检查:
```rust
fn f() -> fn(i32) -> ! {
|_| panic!()
}
fn g() -> fn(i32) -> i32 {
f()
}
fn main() {}
```
这会输出以下错误:
```
error[E0308]: mismatched types
--> src/main.rs:6:5
|
5 | fn g() -> fn(i32) -> i32 {
| -------------- expected `fn(i32) -> i32` because of return type
6 | f()
| ^^^ expected `i32`, found `!`
|
= note: expected fn pointer `fn(_) -> i32`
found fn pointer `fn(_) -> !`
```
与底部类型不同,我们只能从 `!` 类型转换为任何其他类型。如果它嵌套在其他类型中,我们就无法进行相同的操作。因此我们需要编写如下代码:
```rust
fn f() -> fn(i32) -> ! {
|_| panic!()
}
fn g() -> fn(i32) -> i32 {
|x| {
let never: ! = f()(x); // 在这里进行强制转换,用其他语言我们会写 absurd(never)。
never
// 更明确地,我们可以写:
// let number: i32 = never;
// return number;
}
}
fn main() {}
```
这样可以编译通过,没有任何错误(尽管你会收到一些未使用的警告,因此需要 `#[allow(unused)]`)!
因此,Rust 并不比 Scala 能力弱。只是使用起来有点笨拙。我希望你能明白为什么这两种类型之间存在真实且实际的区别,以及为什么我认为将 Rust 的 `!` 与空类型而非底部类型进行比较很重要。
## 误解从何而来?
我怀疑,和许多事情一样,来自维基百科!*底部类型*页面(Wikipedia contributors, 2026)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:wikipedia_bottom_type)写道:
> 如果一个类型系统是健全的,底部类型就是无人居住的,底部类型的项表示逻辑矛盾。在这类系统中,通常不区分底部类型和空类型,它们可以互换使用。
这相当具有误导性,因为它看起来这句话的唯一前提是类型系统是健全的。事实上,我们还需要类型系统具有子类型化关系,因为根据定义,底部类型就是这样工作的(我将在下面详细说明)。
现在,公平地说,*底部类型*页面在通篇都提到了子类型化,所以你可以原谅它,并认为从周围的上下文来看这是“显而易见的”。但*空类型*页面(Wikipedia contributors, 2026)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:wikipedia_empty_type)在任何地方都没有提到“子类型”,它说:
> 如果一个类型系统包含空类型,底部类型也必须是无人居住的,因此不区分它们,并且两者都用 \(\bot\) 表示。
再次,它用蓝色链接标注了“底部类型”,所以你可以期望读者假设该语言也需要子类型化。但我认为大多数人读这句话时并不是这样理解的。我认为,这就是一直误导人们的地方。
## 能否给我一个更正式的解释?
当然!如果你对编程语言理论不感兴趣,以下内容其实并非必需,但如果你好奇,我们或许应该给出一个正式的定义。
我们将空类型定义为没有构造器的类型,通常表示为 \(\mathbf{0}\)。我们通常使用 \(\mathbf{0}\) 表示空类型,使用 \(\mathbf{1}\) 表示单元类型(在 Rust 中是 `()`)。空类型唯一的规则(在非依赖类型的语言中)是消去规则:
$$ \dfrac {\Gamma \vdash L \colon \mathbf{0}} {\Gamma \vdash \texttt{case} \; L \; \texttt{\{ \}} \colon A} \quad \text{或} \quad \dfrac {\Gamma \vdash L \colon \mathbf{0}} {\Gamma \vdash \texttt{absurd}(L) \colon A} $$
如上所示,有两种写法。第一种,我们使用模式匹配。注意这里没有需要覆盖的分支,意味着没有需要匹配的模式。第二种,我们使用通常称为*absurd*的函数,其类型为 \(\forall A. \mathbf{0} \to A\)(脚注1)。
快速回顾一下子类型:我们定义子类型化关系 \(<:\)(读作“...的子类型”),使得对于某些类型 \(A\) 和 \(B\),如果 \(A <: B\),那么每当代码需要 \(B\) 时,我们可以使用 \(A\) 代替。例如,在 Java 中,我们有 `ArrayList <: AbstractList`,这意味着我们可以在代码需要 `AbstractList` 的所有地方使用 `ArrayList`。
子类型化构成一个预序(Pierce, p. 185, 2002)(脚注2)。我们定义底部类型,通常表示为 \(\bot\),也声明它没有构造器,但我们不是通过推导规则,而是简单地使其成为子类型化预序中的最小类型。事实上,我们将其定义为*那个*最小类型,使得对于任何类型 \(A\),都有 \(\bot <: A\)。相同的方法可用于定义顶部类型,只是将 \(<:\) 反向(对于所有类型 \(A\),我们有 \(A <: \top\))。
这两个定义都允许你将底部类型转换为你需要的任何类型。**如果**语言具有子类型化,那么空类型和底部类型就是相同的。
那个 Scala 代码是如何工作的?我们如何知道 `(Int => Nothing)` 是 `(Int => Int)` 的子类型?嗯,函数的子类型规则如下:
$$ \dfrac {C <: A \qquad B <: D} {A \to B <: C \to D} $$
这称为返回类型的*协变*。注意子类型关系在参数上是反向的,这称为参数类型的*逆变*。关于型变,我强烈推荐 Benjamin Hodgson 的《型变的第四种类型》(2019)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:hodgson_fourth_type)。
因为 `Nothing` 是 `Int` 的子类型,返回类型确实是协变的,此规则允许我们有 `(Int => Nothing) <: (Int => Int)`。
说了这么多,我们现在需要问自己一个你应该总是问自己的问题:
## 附言:Haskell 怎么样?
Haskell 以惰性求值闻名。有一个特别有趣的组合允许你比在其他语言中更多地摆弄空类型(它称之为 `Void`):
```haskell
module Main where
import Data.Void
import Control.Exception (assert)
nameSide :: Either Void Int -> String
nameSide (Left _) = "left"
nameSide (Right _) = "right"
main :: IO ()
main = do
assert (nameSide leftVoid == "left") $
assert (length voidList == 2) $
return ()
where
voidList :: [Void]
-- ~undefined~ 类似 Rust 中的 ~panic!()~。
voidList = [undefined, undefined]
leftVoid :: Either Void Int
leftVoid = Left undefined
```
即使我们“创建”了 `Void`,只要我们不求值它,程序就能正常运行。在 Rust 中,这是行不通的,因为 `panic!()` 会立即被求值并退出程序(至少是局部代码)。这是一个惰性求值产生与严格求值不同值的例子。
惰性求值的另一个效果是我们需要跟踪 `Either` 的当前变体是什么。是 `Left Void` 还是 `Right Int`?在 Rust 中,我们不需要这样做。我们确切地知道我们不可能拥有 `Ok(())` 类型的值。因此,`std::mem::size_of::<i32>()` 将返回 `4`:与单独 `i32` 的大小相同。
## 总结
因此,这两种类型并不相同,Rust *没有*底部类型(目前如此,尽管我敢断言这种类型永远不会被添加到 Rust 中)。但这并不意味着 Rust 的表达能力不如具有 \(\bot\) 和子类型化的语言。
## 练习
选择一种类型(函数类型除外),它包含 Rust 和另一种面向对象编程语言中存在的另一种类型(使用泛型)。让该类型为 `A`。然后,像上面一样,展示在面向对象语言中与在 Rust 中将类型 `A<!>` 强制转换为类型 `A`(对于你选择的类型 `X`)有何不同。
提示:使用 `Result`(又名 `Either`)。
解决方案:使用 Scala:
```scala
def getEitherNothing(): Either[Nothing, Nothing] = Left(throw Error())
// 就像之前一样,我们可以直接使用类型为 Either[Nothing, Nothing] 的项。
def getEitherRegular(): Either[Int, String] = getEitherNothing()
@main def main() = ()
```
在 Rust 中:
```rust
fn get_result_never() -> Result<!, !> {
Ok(panic!())
}
fn get_result_regular() -> Result<i32, String> {
// 但在 Rust 中,我们需要进行模式匹配。
match get_result_never() {
Ok(n) => n,
Err(n) => n,
};
}
fn main() {}
```
我们知道 `Result<!, !>` 始终是无人居住的,因此我们应该能够将其强制转换为任何其他类型。使用子类型化,我们可以直接利用 `Nothing <: Int` 和 `Nothing <: String` 的事实,然后得出结论 `Either[Nothing, Nothing] <: Either[Int, String]`,因为 `Either` 的两个类型参数都是协变的。在 Rust 中,我们首先需要获取一个单独的 `!` 值(我们可以这样做,因为 `Result` 的两个变体都是 `!`),然后返回它,将其强制转换为所需的类型。
## 脚注
1. 我通常尽量避免脚注,但这是一个非常有趣的兔洞,与主旨关系极小。来自 CMU 的人通常使用 `abort` 作为 `absurd` 的名称,例如 Pfenning 在他的讲义中(Pfenning and Platzer, 2024)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:pfenning_platzer_lecture)和 Harper 在*PFPL*第一版中(Harper, 2013)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:harper_pfpl_first)。然而,Harper 后来表示他后悔这样做了,并更喜欢模式匹配语法 `case L of { }`(他在*PFPL*第二版中使用了这个),因为“abort”听起来太像“panic/exit the program”(Harper, 2026)(https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#ref:harper_commentary_on)。爱丁堡(我最初学习编程语言理论的地方)通常使用模式匹配,尽管 `absurd` 也被使用。无论如何,我真的很喜欢有 `absurd` 函数,因为它不要求你的语言中必须有模式匹配。↑1 (https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#fn1)
2. 重要的是,这是一个预序,而不是偏序。证明与此无关,但我还是提供一下。回顾一下,我们需要证明自反性、传递性和反对称性。我们将使用定义相等性,即两个类型相等当且仅当它们具有相同的定义(在像 Rust 和 Scala 这样更丰富的语言中,这包括类型参数、方法、字段/属性等)。
自反性是平凡的:对于所有类型 \(A\),显然 \(A <: A\),因为 \(A = A\),我们确实可以在需要 A 的地方使用相同的类型,并在程序中具有相同的行为。
传递性可以通过注意到(对于所有类型 \(A, B, C\))如果 \(B <: C\),那么根据 \(<:\) 的定义,我们可以用 \(B\) 代替 \(C\);我们可以为 \(A <: B\) 论证同样的事情,我们可以简单地使用 \(A\)。但如果我们能用 \(A\) 代替 \(B\),并且能用 \(B\) 代替 \(C\),那么我们就能用 \(A\) 代替 \(C\),因此 \(A <: C\),如所需 \(\square\)。
它不是偏序的原因是我们可能有匿名结构体 `{ x: Int, y: Int }` 和 `{ y: Int, x: Int }`,我们可以声明它们互为子类型,但它们并不相等。↑2 (https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#fn2)
## 参考文献
1. waffle (2026) ‘I stabilized never type.’ 24 August. Available at: https://blog.ihatereality.space/0C-never-type/ (Accessed: 2026-08-26). ↑1 (https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#cite1)
2. Wikipedia contributors (2026a) ‘Bottom type.’ *Wikipedia, The Free Encyclopedia*. Available at: https://en.wikipedia.org/w/index.php?title=Bottom_type&oldid=1359233027 (Accessed: 2026-08-31). ↑2 (https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#cite2)
3. Wikipedia contributors (2026b) ‘Empty type.’ *Wikipedia, The Free Encyclopedia*. Available at: https://en.wikipedia.org/w/index.php?title=Empty_type&oldid=1326215327 (Accessed: 2026-08-31). ↑3 (https://ettolrach.com/blog/rust_when_empty_isnt_bottom.html#cite3)
4. Leinster, T. (2014) ‘Rethinking Set Theory’, *The American Mathematical Monthly*, 121(5), p. 403. Av
相似文章
擦除存在类型
深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。
未定大小值的类型转换
本文探讨了Rust中对未定大小值进行类型转换的挑战,与Go语言的接口动态类型进行比较,指出了Rust类型系统在处理非定大小类型方面的限制。
稳定Rust的Never类型
Rust在经过两年多的开发后,稳定了其'never'类型,这一特性使得泛型代码更高效,并简化了语言中的类型推断。
Rust 项目目标:不可移动类型与保证析构函数
Rust 项目目标概述了不可移动类型和保证析构函数的计划,以提升语言安全性和资源管理。
Rust中基于GADT风格枚举的零成本'Tagless Final'实现
本文探讨了在Rust中使用GADT风格枚举实现'tagless final'模式,展示了编译器如何通过擦除抽象来实现零成本性能。