领导问我 Kotlin 的 Nothing 是什么?

0 阅读7分钟

cover.png

前几天我正在写代码,我领导突然给我发消息。

领导:Nothing 我:is impossible? 领导:Kotlin 这东西什么意思? 我:撤回一条消息 我:等会儿,领导,现在项目很急,我抽空给你写篇文章。

但是当我真的开始写的时候,我发现 Nothing、Any、Unit 这三个类型可以一起讲讲,因为相对于 Java,这三个类型确实比较特殊。

简单来讲:

Any 是所有非空类型共同的超类型,可以接收任何非空值。Unit 表示“事情做完了,没有结果要交回来”。Nothing 表示“这段代码不会正常执行完”,比如它总会抛出异常。

在 GitHub 找一个较为复杂的 Android 项目,你很可能同时看到这三个类型。

按钮的回调是 onClick: () -> Unit,加载状态是 UiState<Nothing>,埋点参数的值是 Any?。

这些写法看起来好像比较随意,但实际上一旦你熟了,你就知道它们既告诉开发者,也告诉编译器一些有用的信息。

现在你肯定想问:这三个类型分别是什么意思?跟 Java 有什么区别?

OK,我们从 Android 里常见的几段代码说起。

它们在 Kotlin 类型体系的哪一层

可以把 Kotlin 的类型体系想成一架梯子。

  • 最上面是 Any?,所有值都能放进去,包括 null。
  • 非空类型这边的顶端是 Any,除了 null,什么值都能放进去。
  • 中间是平时用的类型,比如内置的 String 和 Int、自己写的类(像原作者在这个系列的点餐应用里用到的 Order),还有 Unit。
  • 最下面是 Nothing,它没有任何值。Nothing 是所有类型的子类型,这点很重要!!!

类型越靠上,能接收的值越多,但你对这个值知道得越少。

而 Nothing 是另一个极端:它告诉编译器,“你不会从这里拿到任何值。”

什么是 Any

所有 Kotlin 类最终都继承自 Any,即使你没有显式写出来也是如此。所以,每个对象都有 equals()、hashCode() 和 toString() 这三个函数。

类型为 Any 的参数可以接收任意非空值。

Android 里一个常见的场景是埋点:同一条事件里可能有数字、文本,也可能有对象。

// Sends one analytics event with any mix of values
fun logEvent(name: String, vararg params: Pair<String, Any?>) { /* ... */ }

logEvent("checkout", "items" to 3, "coupon" to "SAVE10", "note" to null)

这里每个参数的值用的是 Any?,因为备注可能是 null。如果想按实际类型使用这个值,可以先用 is 检查。比如在 if (value is String) 的分支里,编译器允许你直接写 value.length。这就是智能转换(smart cast)。

在 JVM 上,Kotlin 把 Java 的 Object 映射为 Any。

当然,两者还是有区别的:Any 只定义了上面三个成员函数,且不能直接在它上面调用 wait() 或 notify()。

如果是写日志、记埋点,跟 Bundle 打交道时,Any 很方便。

但在应用内部,用它会丢掉具体的类型信息。如果你写的函数接收 Any,然后内部还用 is 检查各种类型,那么,我劝你,写成密封类型或泛型通常更合适。

Unit Vs Java void

假设,函数做了点事,但没有结果要交给调用方,这时可以返回 Unit。

当然,在 Kotlin 中,一般不用写出来:

fun clearCart() { cartItems.clear() }   // return type is Unit

Java 的 void 表示没有返回值。

Kotlin 的 Unit 是一个类型,它唯一的值也叫 Unit。

下面是 Kotlin 2.4.0 标准库中 JVM 版的 Unit 定义。

public actual object Unit {
    override fun toString(): String = "kotlin.Unit"
}

这里用 object 声明单例。

Unit 既然是个值,你甚至可以把它存起来:val result = clearCart() 可以编译,打印 result 会得到 kotlin.Unit。

多了一个值,会有额外开销吗?像 clearCart() 这样直接调用、又不接收结果,就没有额外的返回值开销。它在 JVM 上会编译成一个返回 void 的方法,既不用创建 Unit 对象,也不用返回它。

那为什么还需要一个类型?因为 Unit 可以用在泛型里,void 不行。

  • onClick: () -> Unit 是一个函数类型,表示回调不需要给调用方返回有用的结果。
  • suspend fun cancelOrder(id: String): Result<Unit> 表示取消操作可能成功,也可能失败,但成功时没有额外的数据。
  • MutableSharedFlow<Unit> 可以用来从 ViewModel 发出不带数据的事件,比如“订单已提交,关闭购物车页面”。收到事件本身就够了,所以可以用 Unit。默认的 MutableSharedFlow() 在没有订阅者时不会保留事件。

在 Java 的泛型里表达“没有结果”,可以用 Void,再写个 return null。Kotlin 直接用 Unit 就行。

好,领导关心的重点来了!

Nothing 是什么

Nothing 表示代码不会正常结束。它没有任何值,所以声明返回 Nothing 的函数不会正常返回,比如它可能抛出异常,或者一直循环下去。

其实你已经用过它了。

throw 是一个类型为 Nothing 的表达式,标准库里的 error() 和 TODO() 也都声明返回 Nothing。

nothing-control-flow.png

Nothing 在梯子的最下面,是所有类型的子类型。所以,下面的代码能通过编译:

data class Order(val id: String, val total: Int)
fun findOrder(id: String): Order? = orders[id]   // null when not found

val order: Order = findOrder(orderId) ?: error("Order $orderId not loaded")

?: 有两条路径。找到订单时,左边给出一个 Order;找不到时,右边的 error() 抛出异常,不会给出任何值,表达式的类型是 Nothing。因此整个 ?: 表达式的类型是 Order,order 不需要声明成 Order?。

自己写辅助函数时,返回类型也会影响编译器的判断。下面是 stopOrder 的两个版本,第二个用来替换第一个:

fun orderTotal(id: String): Int {
    val order = findOrder(id)
    if (order == null) stopOrder(id)
    return order.total   // error: order is still Order?
}

// ! Almost right: the helper always throws, but the compiler doesn't know
fun stopOrder(id: String) {
    throw IllegalStateException("Order $id not loaded")
}

用下面这个版本替换第一个 stopOrder,再编译 orderTotal。

// ✓ Right: Nothing tells the compiler this call never comes back
fun stopOrder(id: String): Nothing {
    throw IllegalStateException("Order $id not loaded")
}
// Now order.total compiles, because order can't be null after that line

第一个版本的 stopOrder 是块体函数,省略返回类型时默认是 Unit。

从调用方看,执行完 stopOrder(id) 之后还有可能继续往下走,而此时 order 仍可能为 null。所以直接访问 order.total 会报错,要求使用安全调用 ?. 或非空断言 !!;一种快速修复是写成 order!!.total。

但 stopOrder(id) 加上 : Nothing 以后,编译器就知道:如果 order 是 null,代码就走不到 return order.total。因此在访问 total 时,order 已经被智能转换为 Order,不用写 !!。

为什么你会看到 UiState

这是 Android 代码里常见的一种写法。

很多应用把表示界面状态的类型叫作 UiState,这里用的是 OrderState。

sealed interface OrderState<out T> {
    data object Loading : OrderState<Nothing>
    data class Ready<T>(val data: T) : OrderState<T>
    data class Failed(val message: String) : OrderState<Nothing>
}

val state: OrderState<Order> = OrderState.Loading   // compiles, it's amazing

Loading 不携带数据,所以它的类型参数可以用 Nothing。

Nothing 是 Order 的子类型,而 T 用 out 声明为协变。因此,OrderState<Nothing> 也是 OrderState<Order> 的子类型。无论 T 是什么,都可以用同一个 Loading 对象。

当然,去掉 out 的话,最后一行就会报错:初始化值的类型不匹配。

快速对照

类型能表示什么Android 中的例子
Any?任意值,包括 null埋点参数、Map<String, Any?>、equals(other: Any?)
Any任意非空值接收非空值的日志函数
Unit唯一的值是 Unit,表示没有需要返回的结果() -> Unit、Result<Unit>、Flow<Unit>
Nothing没有值,表示这条路径不会正常结束throw、error()、TODO()、UiState<Nothing>

几个常见问题

Any 和 Java 的 Object 是一回事吗?

在 JVM 上,两者有对应关系。如果 Java 方法的 Object 参数没有可空性标注,在 Kotlin 里通常会看到平台类型 Any!。Any 本身是 Kotlin 的类型,只定义了三个成员函数,在 JVM 之外的 Kotlin 平台上也有。

Unit 和 Nothing 有什么区别?

返回类型是 Unit 的函数正常返回时,没有需要交给调用方的结果。返回类型是 Nothing 的函数则不会正常返回。在 JVM 字节码里,像 clearCart() 这样的普通 Unit 函数通常编译为返回 void 的方法,而声明返回 Nothing 的普通方法在 JVM 上的返回类型是 java.lang.Void(这个真的是个类型)。

null 是什么类型?

写下 val x = null,编译器会把 x 推断为 Nothing?。Nothing? 只有一个值,就是 null。它是所有可空类型的子类型,所以 null 可以赋给任何 T?。

写代码时记住什么?

Any? 在梯子顶端,能接收任意值;Any 紧挨着它,能接收任意非空值,但不会告诉你具体是什么。

Unit 是一个类型,它唯一的值也叫 Unit,表示没有结果要返回。

Nothing 在最底下,告诉编译器这条路径不会正常返回,编译器就能更准确地判断其他路径上的类型。

写一个总会抛异常的辅助函数时,记得把返回类型写成 : Nothing。这样编译器就能在后续代码里做智能转换,你也能少写几个 !!。