Kotlin 中的 Nothing:一个“没有值”的类型到底有什么用?

75 阅读9分钟

Kotlin 中的 Nothing:一个“没有值”的类型到底有什么用?

在学习 Kotlin 的时候,我们经常会遇到一些特殊类型,比如 AnyUnit,而 Nothing 可能是其中最容易让人困惑的一个。

第一次看到它时,我很容易产生这样的疑问:

Nothing 到底是什么?

为什么函数可以声明返回 Nothing,但我们却不能创建一个 Nothing 对象?

如果 Nothing 没有实例,为什么下面的代码又是合法的?

val nothing: Nothing = TODO()

更有意思的是,Nothing 还会影响 Kotlin 编译器对控制流和类型推导的判断。

本文就从这些问题出发,看看 Nothing 到底是什么,以及它在 Kotlin 中究竟有什么作用。


一、Nothing 到底是什么?

先看一个最简单的例子:

fun fail(): Nothing {
    throw Exception()
}

这里函数的返回类型是 Nothing

直觉上可能会理解成:

“这个函数返回一个 Nothing。”

但这个理解并不准确。

Nothing 最重要的特点是:

它没有任何实例。

也就是说,不存在这样一个正常的值:

val x: Nothing = ???

我们不能写:

val x: Nothing = 123       // ❌
val x: Nothing = "hello"   // ❌
val x: Nothing = null      // ❌

也不存在:

Nothing()

这样的对象创建方式。

因此,Nothing 并不是一个特殊的“空对象”。

更准确地说:

Nothing** 是一个没有任何值的类型。**


二、既然没有值,为什么 TODO() 可以赋给 Nothing?

这就是理解 Nothing 的第一个关键点。

下面的代码是合法的:

val nothing: Nothing = TODO()

但这并不意味着 TODO() 创建了一个 Nothing 对象。

TODO() 的定义大致可以理解为:

fun TODO(reason: String = "An operation is not implemented."): Nothing {
    throw NotImplementedError(reason)
}

所以:

val nothing: Nothing = TODO()

实际执行过程是:

调用 TODO()
    ↓
抛出 NotImplementedError
    ↓
控制流离开当前执行路径

nothing 实际上永远不会得到一个真正的值。

因此这里需要区分两个概念:

语法和类型层面可以把一个 Nothing 类型的表达式赋给 Nothing 变量;但运行时并不存在一个真正的 Nothing 对象。

例如:

val nothing: Nothing = throw Exception()

也是合法的。


三、Nothing 并不等于“函数不会返回”

这里非常容易产生一个误解。

例如:

fun printName(name: String) {
    if (name.isEmpty()) {
        return
    } else {
        println(name)
    }
}

这里的 return 是正常结束函数。

return 作为表达式的类型仍然是 Nothing

所以:

val nothing: Nothing = return

也是合法的。

这说明:

Nothing** 并不意味着“函数不会返回”。**

return 会正常结束当前函数,只不过它不会产生一个值交给当前表达式的后续计算。

因此,与其说:

Nothing 表示函数不会返回。

不如说:

Nothing** 表示这个表达式不会产生一个值供后续计算继续使用。**


四、Nothing 与控制流

理解这一点之后,再看 Nothing 对编译器的作用就容易多了。

例如:

fun fail(): Nothing {
    throw Exception()
}

fun test() {
    fail()
    println("Hello")
}

fail() 的类型是:

Nothing

编译器知道:

fail()
    ↓
不会产生正常结果
    ↓
println("Hello") 不可能执行

因此:

println("Hello")

会被识别为:

Unreachable code

五、Nothing 并不要求程序结束

这里还有一个很有意思的例子:

fun runForever(): Nothing {
    while (true) {
        // no-op
    }
}

调用:

fun main() {
    println("start")

    runForever()

    println("end")
}

程序并不会“结束”。

实际情况是:

start
 ↓
runForever()
 ↓
while (true)
 ↓
while (true)
 ↓
while (true)
 ↓
……

程序一直运行在 while 循环里。

但是:

println("end")

永远不会执行。

所以 Nothing 与控制流之间更准确的关系是:

一个 Nothing 类型的表达式不会正常完成,因此从这个表达式继续向下执行的路径是不可达的。

至于为什么不会继续,有很多可能:

return
    ↓
离开当前函数

throw
    ↓
进入异常控制流

TODO()
    ↓
抛出异常

while (true)
    ↓
永远停留在循环中

它们的具体控制流行为不同,但都具有一个共同特点:

不会产生一个正常的值交给后续代码。


六、为什么 Nothing 可以成为所有类型的子类型?

这就进入 Nothing 最核心的类型系统概念了。

Nothing 是 Kotlin 的 Bottom Type(底类型)

也就是说:

Nothing <: String
Nothing <: Int
Nothing <: User
Nothing <: List<String>

简单来说:

Nothing** 是所有类型的子类型。**

因此:

val name: String = TODO()

是合法的。

因为:

TODO()
   ↓
Nothing
   ↓
String

当然,这并不意味着 TODO() 真正产生了一个 String

实际情况仍然是:

TODO()
 ↓
throw
 ↓
不会产生值

只是从类型系统角度来看:

Nothing 可以出现在需要 String 的位置

因为 NothingString 的子类型。


七、为什么 Nothing 是所有类型的子类型?

可以用一个集合的概念来帮助理解。

把一个类型看成它所有可能值的集合。

例如:

Int

{ ..., -2, -1, 0, 1, 2, ... }

String 则是所有可能字符串的集合。

那么 Nothing 可以理解成:

{}

也就是一个空集合。

空集合是任何集合的子集:

{} ⊆ Int
{} ⊆ String
{} ⊆ User

所以:

Nothing <: Int
Nothing <: String
Nothing <: User

这就是 Bottom Type 的直观理解。

当然,实际的 Kotlin 类型系统比这个集合模型复杂得多,但这个模型非常适合帮助理解 Nothing 为什么能够成为所有类型的子类型。


八、Nothing 在泛型中的作用

这可能是普通 Kotlin 开发中 Nothing 最实际的用途。

考虑:

sealed class Result<out T>

data class Success<T>(
    val value: T
) : Result<T>()

data class Failure(
    val exception: Exception
) : Result<Nothing>()

Success 中存在一个 T

Success<T>
    ↓
有一个 T 类型的 value

Failure 中根本没有 T

Failure
    ↓
没有成功结果

那么应该怎么填写 Result 的类型参数?

可以使用:

Result<Nothing>

它表达的意思非常准确:

这里的 T 不可能存在一个值。

由于:

Nothing <: String

并且 Result 声明为:

Result<out T>

它是协变的,因此:

Result<Nothing>
    ↓
Result<String>

所以:

val result: Result<String> = Failure(Exception())

也是合法的。


九、emptyList() 为什么可以不指定类型?

我们平时经常写:

val list = emptyList()

那么 emptyList() 到底是什么类型?

可以把它理解成:

List<Nothing>

为什么?

因为这个 List 中根本没有任何元素。

既然没有元素,就不存在一个具体的元素类型。

因此:

emptyList()
    ↓
List<Nothing>

再利用 Nothing 是所有类型的子类型,以及 List 的协变:

List<Nothing>
    
List<String>

所以:

val names: List<String> = emptyList()

完全没有问题。

类似的情况还有:

emptySet()
emptySequence()
emptyFlow()

它们都可以使用 Nothing 表示:

这里没有任何元素。


十、Nothing 和 Unit 完全不同

Nothing 最容易和 Unit 混淆。

但它们表达的意思实际上几乎是两个极端。

Unit

fun log() {
    println("Hello")
}

这个函数正常执行结束。

它的返回类型实际上是:

Unit

也就是说:

存在一个返回值,只不过这个值没有什么实际信息。


Nothing

fun fail(): Nothing {
    throw Exception()
}

则表示:

不存在一个正常产生的返回值。

所以可以这样记:

Unit
↓
有一个值
↓
这个值通常没有什么有用的信息


Nothing
↓
没有正常产生的值

这两个类型的语义完全不同。


十一、Nothing? 又是什么?

Kotlin 还有:

Nothing?

这和:

Nothing

不是一回事。

可以简单理解成:

Nothing
    ↓
没有任何值

Nothing?
    ↓
只有 null

因此:

val a: Nothing? = null  // ✅

但是:

val b: Nothing = null   // ❌

如果继续用集合的模型理解:

Nothing  = {}
Nothing? = { null }

所以 Nothing? 仍然是一个非常特殊的类型。


十二、为什么我们平时很少主动写 Nothing?

如果你平时主要写 Android 业务代码,可能会发现:

Nothing

几乎不会主动出现在自己的代码中。

这是非常正常的。

因为 Nothing 更像是一个服务于类型系统和库设计的基础设施

你实际上可能已经大量使用了它,只是没有直接写出来:

TODO()
throw Exception()
emptyList()
emptySet()
emptyFlow()

这些 API 的设计都可能利用到 Nothing

真正比较常见的主动使用场景主要是:

Result<Nothing>
Flow<Nothing>
List<Nothing>
sealed class 的某些子类
永远不会产生值的函数

而普通业务代码通常没有必要为了使用 Nothing 而使用它。


十三、为什么序列化中的 Nothing 看起来很奇怪?

Kotlin 官方文档还给出了一个比较特殊的例子:

@Serializable
sealed class ParametrizedParent<out R>

@Serializable
data class ChildWithoutParameter(
    val value: Int
) : ParametrizedParent<Nothing>()

为什么这里使用:

ParametrizedParent<Nothing>

而不是:

ParametrizedParent<Unit>

或者:

ParametrizedParent<Any>

原因是这个子类实际上根本没有 R

ChildWithoutParameter
    │
    └── value: Int

没有 R

所以:

ParametrizedParent<Nothing>

表达的是:

语法上父类要求一个类型参数,但这个子类实际上不可能携带任何 R 类型的值。

这里的 Nothing 是一个非常合适的类型参数。

而序列化时真正需要序列化的是:

value: Int

所以最终仍然可以得到:

{"value":42}

需要注意的是,Nothing 本身虽然可以拥有对应的 serializer,但因为 Nothing 没有实例,所以真正对 Nothing 值进行编码或解码并没有意义,相关操作会失败。


十四、总结:Nothing 到底有什么用?

如果站在普通 Kotlin 开发者的角度,可以把 Nothing 的作用总结成三个层次。

1. 控制流

告诉类型系统:

这个表达式不会正常产生结果
↓
后续正常控制流不可达

例如:

fun forever(): Nothing {
    while (true) {}
}

以及:

throw Exception()
TODO()
return

2. 泛型

当语法上需要一个类型参数,但实际上这个位置不可能存在任何值时:

Result<Nothing>
List<Nothing>
Flow<Nothing>

例如:

data class Failure(
    val exception: Exception
) : Result<Nothing>()

3. 类型系统

最根本的原因是:

Nothing 是 Bottom Type

即:

Nothing <: T

对于各种类型 T 都成立。

这使得 Nothing 能够参与:

  • 类型推导
  • 子类型检查
  • 泛型
  • 协变
  • 表达式类型计算
  • 控制流分析

十五、最后用一句话理解 Nothing

如果只记住一句话,我建议记住:

Nothing** 不是一个“空对象”,而是 Kotlin 类型系统中的 Bottom Type,用来表示“不可能产生正常值”的情况。**

因此:

throw
return
TODO()
while (true)

虽然控制流行为完全不同,但它们都可以具有 Nothing 类型。

而:

Result<Nothing>
List<Nothing>
Flow<Nothing>

则利用了 Nothing “不存在任何值”以及“是所有类型的子类型”的性质。

所以,Nothing 并不是一个我们在业务代码里经常需要主动使用的类型。

它更像是 Kotlin 类型系统提供的一块基础设施:

我们平时不一定直接使用它,但 Kotlin 的类型推导、泛型设计和控制流分析都可以从它那里获得很多信息。