5

这玩意真的有用吗?对,是的!Kotlin 的 Nothing 详解

 1 year ago
source link: https://rengwuxian.com/kotlin-nothing/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

YouTube

下面是视频内容的脚本文案原稿分享。

fe402c850832a.jpg

Kotlin 的 Nothing 类,无法创建出任何实例:

public class Nothing private constructor()
Kotlin

所以所有 Nothing 类型的变量或者函数,都找不到可用的值:

val nothing: Nothing = ???
fun nothing(): Nothing {
  ...
  return ???
}
Kotlin

就这么简单。但——它有啥用啊?

Nothing 的本质

大家好,我是扔物线朱凯。上期讲了 Kotlin 的 Unit,这期讲 Nothing
Nothing 的源码很简单:

public class Nothing private constructor()
Kotlin

可以看到它本身虽然是 public 的,但它的构造函数是 private 的,这就导致我们没法创建它的实例;而且它不像 Unit 那样是个 object

public object Unit {
  override fun toString() = "kotlin.Unit"
}
Kotlin

而是个普通的 class;并且在源码里 Kotlin 也没有帮我们创建它的实例。

这些条件加起来,结果就是:Nothing 这个类既没有、也不会有任何的实例对象。

基于这样的前提,当我们写出这个函数声明的时候:

fun nothing(): Nothing {

}
Kotlin

我们不可能找到一个合适的值来返回。你必须返回一个值,但却永远找不到合适的返回值。悖论了。

作用一:作为函数「永不返回」的提示

不怎么办。这个悖论,就是 Nothing 存在的意义:它找不到任何可用的值,所以,以它为返回值类型的一定是个不会返回的函数,比如——它可以总是抛异常。
什么意思?就是说,我这么写是可以的:

fun nothing() : Nothing {
  throw RuntimeException("Nothing!")
}
Kotlin

这个写法并没有返回任何结果,而是抛异常了,所以是合法的。

可能有的人会觉得有问题:抛异常就可以为所欲为吗?抛异常就可以忽略返回值了吗?——啊对,抛异常就是可以忽略返回值,而且这不是 Nothing 的特性,而是本来就是这样,而且你本来就知道,只是到这里的时候,你可能会忘了。
例如这个写法:

fun getName() : String {
  if (nameValue != null) {
    return nameValue
  } else {
    throw NullPointerException("nameValue 不能为空!")
  }
}
Kotlin

——其实这个函数可以有更加简洁的写法:

fun getName() = nameValue ?: throw NullPointerException("nameValue 不能为空!")
Kotlin

不过我们为了方便讲解,就不简化了:

fun getName() : String {
  if (nameValue != null) {
    return nameValue
  } else {
    throw NullPointerException("nameValue 不能为空!")
  }
}
Kotlin

在这个函数里,一个 if 判断,true 就返回,false 就抛异常,这个写法很常见吧?它在 else 的这个分支,是不是就只抛异常而不返回值了?实际上 Java 和 Kotlin 的任何方法或者说函数,在抛异常的时候都是不返回值的——你都抛异常的还返回啥呀返回?是吧?

所以我如果改成这样:

fun getName() : String {
  throw NullPointerException("不能为空!")
}
Kotlin

其实也是可以的。只是看起来比较奇怪罢了,会让人觉得「怎么会这么写呢」?但它的写法本身是完全合法的。而且如果我把函数的名字改一下,再加个注释:

/**
 当遇到姓名为空的时候,请调用这个函数来抛异常
*/
fun throwOnNameNull() : String {
  throw NullPointerException("姓名不能为空!")
}
Kotlin

这就很合理了吧?不干别的,就只是抛异常。这是一种很常用的工具函数的写法,包括 Kotlin 和 Compose 的官方源码里也有这种东西。

那么我们继续来看它的返回值类型:我都不返回了,就没必要还写 String 了吧?那写什么?可以把它改成 Unit

/**
 当任何变量为空的时候,请统一调用这个函数来抛异常
*/
fun throwOnNameNull() : Unit {
  throw NullPointerException("姓名不能为空!")
}
Kotlin

有问题吗?没问题。

不过,Kotlin 又进了一步,提供了一个额外的选项:你还可以把它改成 Nothing

/**
 当任何变量为空的时候,请统一调用这个函数来抛异常
*/
fun throwOnNameNull() : Nothing {
  throw NullPointerException("姓名不能为空!")
}
Kotlin

虽然我找不到 Nothing 的实例,但是这个函数本来就是永远抛异常的,找不到实例也没关系。哎,这不就能用了吗?对吧?

不过,能用归能用,这么写有啥意义啊?是吧?价值在哪?——价值就在于,Nothing 这个返回值类型能够给使用它的开发者一个明确的提示:这是个永远不会返回的函数。这种提示本身,就会给开发提供一些方便,它能很好地避免函数的调用者对函数的误解而导致的一些问题。我们从 Java 过来的人可能第一时间不太能感受到这种东西的用处,其实你要真说它作用有多大吧,我觉得不算大,主要是很方便。它是属于「你没有的话也不觉得有什么不好的,但是有了之后就再也不想没有它」的那种小方便。就跟 120Hz 的屏幕刷新率有点像,多少带点毒。

Kotlin 的源码、Compose 的源码里都有不少这样的实践,比如 Compose 的 noLocalProviderFor() 函数:

private fun noLocalProvidedFor(name: String): Nothing {
  error("CompositionLocal $name not present")
}
Kotlin

好,这就是 Nothing 的作用之一:作为函数的返回值类型,来明确表达「这是个永不返回的函数」。

其实 Nothing 的「永不返回」除了抛异常之外,还有一种场景,就是无限循环:

fun foreverRepeat(): Nothing {
  while (true) {
    ...
  }
}
Kotlin

不过一般很少有人这么去用,大部分都是用在我刚才说的抛异常的场景,这是非常常见的一种用法,你写业务可能用不到,但是基础架构团队给全公司写框架或者对外写 SDK 的话,用到它的概率就非常大了。

作用二:作为泛型对象的临时空白填充

另外 Nothing 除了「没有可用的实例」之外,还有个特性:它是所有类型共同的子类型。这其实是违反了 Kotlin 的「类不允许多重继承」的规定的,但是 Kotlin 强行扩充了规则:Nothing 除外,它不受这个规则的约束。虽然这违反了「类不允许多重继承」,但因为 Nothing 不存在实例对象,所以它的多重继承是不会带来实际的风险的。——我以前还跟人说「Nothing 是所有类型的子类型」这种说法是错误的,惭愧惭愧,是我说错了。

不过,这个特性又有什么作用呢?它就能让你对于任何变量的赋值,都可以在等号右边写一个 Nothing

val nothing: Nothing = TODO()
var apple: Apple = nothing
Kotlin

这儿其实有个问题:我刚说了 Nothing 不会有任何的实例,对吧?那么这个右边就算能填 Nothing 类型的对象,可是这个对象我用谁啊?

val nothing: Nothing = ???
var apple: Apple = nothing
Kotlin

谁也没法用。

但是我如果不直接用 Nothing,而是把它作为泛型类型的实例化参数:

val emptyList: List<Nothing> = ???
var apples: List<Apple> = emptyList
Kotlin

这就可以写了。一个元素类型为NothingList,将会导致我无法找到任何的元素实例来填充进去,但是这个 List 本身是可以被创建的:

val emptyList: List<Nothing> = listOf()
var apples: List<Apple> = emptyList
Kotlin

只不过这种写法看起来好像有点废,因为它永远都只能是一个空的 List。但是,如果结合上我们刚说的「Nothing 是所有类型的子类型」这个特性,我们是不是可以把这个空的 List 赋值给任何的 List 变量?

val emptyList: List<Nothing> = listOf()
var apples: List<Apple> = emptyList
var users: List<User> = emptyList
var phones: List<Phone> = emptyList
var images: List<Image> = emptyList
Kotlin

这样,是不是就提供了一个通用的空 List 出来,让这一个对象可以用于所有 List 的初始化?有什么好处?既省事,又省内存,这就是好处。

这种用法不只可以用在 ListSetMap 也都没问题:

val emptySet: Set<Nothing> = setOf()
var apples: Set<Apple> = emptySet
var users: Set<User> = emptySet
var phones: Set<Phone> = emptySet
var images: Set<Image> = emptySet
Kotlin
val emptyMap: Map<String, Nothing> = emptyMap()
var apples: Map<String, Apple> = emptyMap
var users: Map<String, User> = emptyMap
var phones: Map<String, Phone> = emptyMap
var images: Map<String, Image> = emptyMap
Kotlin

而且也不限于集合类型,只要是泛型都可以,你自定义的也行:

val emptyProducer: Producer<Nothing> = Producer()
var appleProducer: Producer<Apple> = emptyProducer
var userProducer: Producer<User> = emptyProducer
var phoneProducer: Producer<Phone> = emptyProducer
var imageProducer: Producer<Image> = emptyProducer
Kotlin

它的核心在于,你利用 Nothing 可以创建出一个通用的「空白」对象,它什么实质内容也没有,什么实质工作也做不了,但可以用来作为泛型变量的一个通用的空白占位值。这就是 Nothing 的第二个主要用处:作为泛型变量的通用的、空白的临时填充。多说一句:这种空白的填充一定是临时的才有意义,你如果去观察一下就会发现,这种用法通常都是赋值给 var 属性,而不会赋值给 val

val emptyProducer: Producer<Nothing> = Producer()
// 没人这么写:
val appleProducer: Producer<Apple> = emptyProducer
val userProducer: Producer<User> = emptyProducer
val phoneProducer: Producer<Phone> = emptyProducer
val imageProducer: Producer<Image> = emptyProducer
Kotlin

因为赋值给 val 那就是永久的「空白」了,永久的空白那不叫空白,叫废柴,这个变量就没意义了。

作用三:语法的完整化

另外,Nothing 的「是所有类型的子类型」这个特点,还帮助了 Kotlin 语法的完整化。在 Kotlin 的下层逻辑里,throw 这个关键字是有返回值的,它的返回值类型就是 Nothing。虽然说由于抛异常这件事已经跳出了程序的正常逻辑,所以 throw 返回不返回值、返回值类型是不是 Nothing 对于它本身都不重要,但它让这种写法成为了合法的:

val nothing: Nothing = throw RuntimeException("抛异常!")
Kotlin

并且因为 Nothing 是所有类型的子类型,所以我们这么写也行:

val nothing: String = throw RuntimeException("抛异常!")
Kotlin

看起来没用是吧?如果我再把它改改,就有用了:

var _name: String? = null
val name: String = _name ?: throw NullPointerException("_name 在运行时不能为空!")
Kotlin

throw 的返回值是 Nothing,我们就可以把它写在等号的右边,在语法层面假装成一个值来使用,但其实目的是在例外情况时抛异常。

Kotlin 里面有个 TODO() 函数对吧:

val someValue: String = TODO()
Kotlin

这种写法不会报错,并不是 IDE 或者编译器做了特殊处理,而是因为 TODO() 的内部是一个 throw
07c03d49d7286.jpg
TODO() 返回的是 Nothing,是 String 的子类,怎么不能写了?完全合法!虽然 throw 不会真正地返回,但这让语法层面变得完全说得通了,这也是 Nothing 的价值所在。

除了 throw 之外,return 也是被规定为返回 Nothing 的一个关键字,所以我也可以这么写:

fun sayMyName(first: String, second: String) {
  val name = if (first == "Walter" && second == "White") {
    "Heisenberg"
  } else {
    return // 语法层面的返回值类型为 Nothing,赋值给 name
  }
  println(name)
}
Kotlin

这段代码也是可以简化的:

fun sayMyName(first: String, second: String) {
  if (first == "Walter" && second == "White") println("Heisenberg")
}
Kotlin

不过同样,咱不是为了讲东西么,就不简化了:

fun sayMyName(first: String, second: String) {
  val name = if (first == "Walter" && second == "White") {
    "Heisenberg"
  } else {
    return // 语法层面的返回值类型为 Nothing,赋值给 name
  }
  println(name)
}
Kotlin

虽然直接强行解释为「return 想怎么写就怎么写」也是可以的,但 Kotlin 还是扩充了规则,规定 return 的返回值是 Nothing,让代码从语法层面就能得到解释。

这就是 Nothing 的最后一个作用:语法层面的完整化。

好,Nothing 的定义、定位和用法就是这些。如果没记全,很正常,再看一遍。你看视频花的时间一定没有我研究它花的时间多,所以多看两遍应该不算浪费时间。
下期我会讲一个很多人不关注但很有用的话题:Kotlin 的数值系统,比如 FloatDouble 怎么选、为什么 0.7 / 5.00.14 这类的问题。关注我,了解更多 Android 开发相关的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!

fe402c850832a.jpg

本文首发于:https://rengwuxian.com/kotlin-nothing/

微信公众号:扔物线

转载时请保留此声明


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK