视频先行

这是一篇视频方式的分享,假如你便利看,能够直接去看视频:

  • 哔哩哔哩:这儿
  • 抖音:这儿
  • YouTube:这儿

视频先行

哔哩哔哩

YouTube

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

案牍原稿

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

public class Nothing private constructor()

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

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

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

Nothing 的本质

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

public class Nothing private constructor()

能够看到它自身尽管是 public 的,但它的结构函数是 private 的,这就导致咱们无法创立它的实例;并且它不像 Unit 那样是个 object

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

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

这些条件加起来,成果便是:Nothing 这个类既没有、也不会有任何的实例目标。

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

fun nothing(): Nothing {
}

咱们不或许找到一个适宜的值来回来。你必须回来一个值,但却永久找不到适宜的回来值。悖论了。

效果一:作为函数「永不回来」的提示

怎样办?

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

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

这个写法并没有回来任何成果,而是抛反常了,所以是合法的。

或许有的人会觉得有问题:抛反常就能够为所欲为吗?抛反常就能够忽略回来值了吗?——啊对,抛反常便是能够忽略回来值,并且这不是 Nothing 的特性,而是原本便是这样,并且你原本就知道,只是到这儿的时分,你或许会忘了。 例如这个写法:

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

——其实这个函数能够有更加简练的写法:

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

不过咱们为了便利讲解,就不简化了:

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

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

所以我假如改成这样:

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

其实也是能够的。只是看起来比较古怪算了,会让人觉得「怎样会这么写呢」?但它的写法自身是彻底合法的。并且假如我把函数的名字改一下,再加个注释:

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

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

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

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

有问题吗?没问题。

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

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

尽管我找不到 Nothing 的实例,可是这个函数原本便是永久抛反常的,找不到实例也不要紧。哎,这不就能用了吗?对吧?

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

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

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

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

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

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

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

效果二:作为泛型目标的暂时空白填充

别的 Nothing 除了「没有可用的实例」之外,还有个特性:它是一切类型共同的子类型。这其实是违反了 Kotlin 的「类不允许多重承继」的规矩的,可是 Kotlin 强行扩大了规矩:Nothing 除外,它不受这个规矩的约束。尽管这违反了「类不允许多重承继」,但由于 Nothing 不存在实例目标,所以它的多重承继是不会带来实际的危险的。——我以前还跟人说「Nothing 是一切类型的子类型」这种说法是错误的,羞愧羞愧,是我说错了。

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

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

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

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

谁也无法用。

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

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

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

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

只不过这种写法看起来如同有点废,由于它永久都只能是一个空的 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

这样,是不是就供给了一个通用的空 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
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

并且也不限于集合类型,只要是泛型都能够,你自定义的也行:

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

它的中心在于,你运用 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

由于赋值给 val 那便是永久的「空白」了,永久的空白那不叫空白,叫废柴,这个变量就没含义了。

效果三:语法的完好化

别的,Nothing 的「是一切类型的子类型」这个特点,还帮助了 Kotlin 语法的完好化。在 Kotlin 的下层逻辑里,throw 这个关键字是有回来值的,它的回来值类型便是 Nothing。尽管说由于抛反常这件事现已跳出了程序的正常逻辑,所以 throw 回来不回来值、回来值类型是不是 Nothing 对于它自身都不重要,但它让这种写法成为了合法的:

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

并且由于 Nothing 是一切类型的子类型,所以咱们这么写也行:

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

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

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

throw 的回来值是 Nothing,咱们就能够把它写在等号的右边,在语法层面假装成一个值来运用,但其实意图是在例外情况时抛反常。

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

val someValue: String = TODO()

这种写法不会报错,并不是 IDE 或许编译器做了特殊处理,而是由于 TODO() 的内部是一个 throw

这玩意真的有用吗?对,是的!Kotlin 的 Nothing 详解
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)
}

这段代码也是能够简化的:

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

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

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

尽管直接强行解释为「return 想怎样写就怎样写」也是能够的,但 Kotlin 还是扩大了规矩,规矩 return 的回来值是 Nothing,让代码从语法层面就能得到解释。

这便是 Nothing 的最终一个效果:语法层面的完好化。

总结

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