Rust:空类型并非底类型

Lobsters Hottest 新闻

摘要

这篇文章讨论了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

相似文章

擦除存在类型

Lobsters Hottest

深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。

未定大小值的类型转换

Lobsters Hottest

本文探讨了Rust中对未定大小值进行类型转换的挑战,与Go语言的接口动态类型进行比较,指出了Rust类型系统在处理非定大小类型方面的限制。

稳定Rust的Never类型

Hacker News Top

Rust在经过两年多的开发后,稳定了其'never'类型,这一特性使得泛型代码更高效,并简化了语言中的类型推断。