Kotlin 虚空 VS 单位 VS 虚无

科特林有三种性质非常相似的类型:

  • Void
  • Unit
  • Nothing

他们似乎在犯 JavaScript 的错误:

  • null
  • undefined
  • void(0)

假设他们的 没有陷入了同样的错误,他们都是为了什么,他们有什么不同?

40047 次浏览

它是一个普通的 Java 类,在 Kotlin 没有特殊的意义。

Unit类型只有一个值。取代了 Javavoid(注意: 不是 Void)。更多信息在 Kotlin 医生中。

Nothing没有实例(就像 Void一样)。它代表“一个从不存在的价值”。在 Kotlin,如果你抛出一个错误,它是一个 Nothing(见 Kotlin 医生)。

Void类型来自 Java。你通常不会从 Kotlin 使用它,除非你使用一些 Java 库使用它。

Unit类型是从一个不返回任何感兴趣内容的函数返回的值。这种功能通常会产生某种副作用。单元类型只有一个可能的值,即 Unit对象。在 Kotlin 使用 Unit作为返回类型,而在 Java 中使用 void(小写 v)。

Nothing类型没有值。如果函数的返回类型为 Nothing,则它不能正常返回。它要么必须抛出异常,要么必须输入无限循环。对返回类型为 Nothing的函数的调用之后的代码将被 Kotlin 编译器标记为不可达。

因为 Nothing没有值,所以 Nothing?实际上是在 Kotlin 只捕获 null值的类型。

单位

Unit就像 void

在 Kotlin,当一个函数没有返回任何有意义的值时,它被声明为返回 Unit,就像 Java 中的 void:

fun greet(): Unit { println("Good day!") }

在函数返回 Unit时跳过编写 Unit是一种约定,因为编译器认为 Unit是默认的返回类型:

fun greet() { println("Good day!") }

Unit是单身

这个类只有一个对象(单例模式) ,这个对象就是它自己。它在 kotlin包中使用对象声明进行声明,如下所示:

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

函数式编程中的 Unit

Kotlin 在函数式编程方面拥有一流的支持。在函数式编程语言中使用 Unit是很常见的。它通过使所有函数都声明为具有返回值,使得函数类型更具可读性,即使在函数不返回值的情况下:

val greet: () -> Unit = { println("Good day!") }

在这里,() -> Unit是一个函数类型,->后面的 Unit表示这个函数类型不返回任何有意义的值。在函数类型中不能跳过提到 Unit

用于扩展泛型的 Unit

每个函数都必须返回一个值。Kotlin 决定使用 同学们来表示它,而不是 Java 中的特殊类型 void。使用类的原因是,通过使类型系统成为类型层次结构的一部分,可以使类型系统更加一致。

例如,假设我们有一个名为 Worker<T>的通用 interface,它执行一些工作。这个接口的 doWork()功能做一些工作,必须返回一个值 T:

interface Worker<T> {
fun doWork(): T
}

但是有时候,我们可能希望将这个接口用于某些工作,例如,在下面显示的扩展 Worker接口的 LogWorker类中的日志记录工作:

class LogWorker : Worker<Unit> {
override fun doWork() {
// Do the logging
}
}

这就是 Unit的神奇之处,在这里我们能够使用最初设计用于返回值的预先存在的接口。在这里,我们让 doWork()函数返回 Unit值,以满足我们的目的,在这里我们没有任何东西可以返回。因此,当重写返回泛型参数的函数时,它非常有用。

注意,我们还跳过了 doWork()函数的 Unit返回类型。也不需要编写 return语句。

没什么

Nothing的价值从不存在

在 Kotlin,类 Nothing代表 一个从不存在的价值。这个类永远不可能有任何值/对象,因为它的 constructor保持为 private。它在 kotlin包中定义如下:

public class Nothing private constructor()

Nothing用于从不返回值的函数的返回类型。例如,具有无限循环的函数或总是引发异常的函数。来自 Kotlin 标准库的 error()函数就是一个例子,它总是抛出一个异常并返回 Nothing。下面是它的代码:

fun error(message: Any): Nothing = throw IllegalStateException(message.toString())

Nothing是底部类型

在类型理论中,没有值的类型称为底类型,它是所有其他类型的子类型。因此,在 Kotlin,Nothing是所有类型的 亚型,就像 Any?是所有类型的 超模一样。因此,Nothing类型的值(从不存在)可以赋给所有类型的变量,例如:

val user: User = request.user ?: error("User not found")

在这里,我们使用 elvis 操作符(?:)调用前面定义的 error()函数(如果 usernull)。error()函数返回类型为 Nothing的值,但是可以将其赋给类型为 User的变量,因为 NothingUser的一个子类型,就像它是任何其他类型的一个子类型一样。编译器允许这样做,因为它知道 error()函数永远不会返回一个值,所以不会造成损害。

类似地,可以从具有任何其他返回类型的函数返回 Nothing:

fun getUser(request: Request): User {
return request.user ?: error("User not found")
}

在这里,即使 getUser()函数被声明为返回 User,如果 usernull,它也可以返回 Nothing

ABc0空对象模式

考虑下面的函数示例,该函数删除列表中给出的文件:

fun deleteFiles(files: List<File>? = null) {
if (files != null) files.forEach { it.delete() }
}

这个函数设计的问题在于它不能表示 List<File>是空的、 null还是有元素。另外,在使用该列表之前,我们需要检查它是否是 null

为了解决这个问题,我们使用空对象设计模式。在空对象模式中,我们不使用 null引用来表示没有对象,而是使用一个对象来实现预期的接口,但是留下方法体为空。

因此,我们定义了接口 List<Nothing>的对象:

// This function is already defined in the Kotlin standard library
fun emptyList() = object : List<Nothing> {
override fun iterator(): Iterator<Nothing> = EmptyIterator
...
}

现在我们在 deleteFiles()函数中使用这个 null 对象作为参数的默认值:

fun deleteFiles(files: List<File> = emptyList()) {
files.forEach { it.delete() }
}

这消除了 null或空的不确定性,使意图更加清晰。它还删除了 null 检查,因为 null 对象上的函数是空的,它们将被调用,但它们是 禁止行动(它们中没有操作,因此它们什么也不做)。

协变泛型的 Nothing

在上面的例子中,编译器允许我们在需要 List<File>的地方传递 List<Nothing>。这是因为 Kotlin 的 List接口是协变的,因为它使用的是 out关键字,也就是 List<out T>。据了解,Nothing是所有类型的亚型,Nothing也是 File的亚型。由于协方差,List<Nothing>List<File>List<File>0、 List<File>1等等的一个亚型... ... List<File>2。这适用于具有协变泛型(out)的任何类型,而不仅仅是 List

Nothing更好的性能

就像我们示例中使用的函数 emptyList()一样,有一些预定义的函数,如 emptyMap()emptySet()emptySequence(),它们返回 null 对象。所有这些都是使用 Nothing定义的。您可以像这样定义自己的对象。

这里的优点是,这些返回单例对象,例如,您可以调用相同的 emptyList()函数来获得一个空实例,无论它是分配给 List<File>List<Int>和... List<AllTypes>还是在多个位置。由于每次都返回相同的对象,因此可以节省对象创建和内存分配的成本。

空洞

扩展 Java的类型擦除的 Void

Void类来自 java.lang包,而 UnitNothing来自 kotlin包。ABc0并不打算在 Kotlin 使用。Kotlin 也有自己的课程,以 ABc2的形式。

在 Java 中,Void用于扩展泛型接口,比如为 Unit编写的 Worker接口示例,其中必须返回一个值。因此,为了将 Kotlin 代码转换成 Java,我们可以使用 Void,就像我们在 Worker示例中使用 Unit一样,然后用 Java 重写代码,如下所示:

interface Worker<T> {
T doWork();
}


class LogWorker implements Worker<Void> {
@Override public Void doWork() {
// Do the logging
return null;
}
}

请注意,在使用 Void时,我们必须使用 Void作为返回类型(不能跳过) ,并且需要编写 return语句,而对于 Unit,我们可以两者都跳过。这是在 Kotlin 代码中避免使用 Void的另一个原因。

结论

因此,在我看来,UnitNothing不是 Kotlin 设计师的错误,也不像 Javascript 中的 nullundefinedvoid(0)那样有问题。UnitNothing使函数式编程变得轻而易举,同时提供了前面提到的其他有用特性。它们在其他函数式编程语言中也很常见。

就是这样,希望能有帮助。