Kotlin 中的 Nothing:一个“没有值”的类型到底有什么用?
在学习 Kotlin 的时候,我们经常会遇到一些特殊类型,比如 Any、Unit,而 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 的位置
因为 Nothing 是 String 的子类型。
七、为什么 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 的类型推导、泛型设计和控制流分析都可以从它那里获得很多信息。