在 Kotlin 中,处理 nullable 值、引用类型或转换它们的惯用方法是什么

如果我有一个可空类型Xyz?,我想引用它或将它转换为非可空类型Xyz。在Kotlin中这样做的惯用方法是什么?

例如,以下代码是错误的:

val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?"

但如果我先检查空,它是允许的,为什么?

val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
something.foo()
}

我如何在不需要if检查的情况下改变或处理一个不是null的值,假设我确定它真的从来没有null?例如,这里我正在从一个映射中检索一个值,我可以保证它存在,而get()的结果不是null。但我有一个错误:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")
something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"

方法get()认为该项可能缺失,并返回类型Int?。因此,将值的类型强制为非空的最佳方法是什么?

注意: 这个问题是作者有意写下并回答的(自我回答的问题),所以通常被问到的Kotlin主题的惯用答案出现在so中。同时也要澄清一些为Kotlin alpha编写的旧答案,这些答案对于现在的Kotlin来说并不准确。

79911 次浏览

首先,你应该阅读Kotlin中关于空安全的所有内容,其中涵盖了所有的情况。

在Kotlin中,如果不确定它不是null (检查条件是否为空),或者使用!!确定操作符断言它肯定不是null,使用?.安全呼叫访问它,或者最后使用?:猫王运算符给可能是null的东西一个默认值,就不能访问一个可空值。

你问题中的第一个案子你可以根据代码的意图来选择,你可以使用其中的一个,所有这些都是惯用的,但有不同的结果:

val something: Xyz? = createPossiblyNullXyz()


// access it as non-null asserting that with a sure call
val result1 = something!!.foo()


// access it only if it is not null using safe operator,
// returning null otherwise
val result2 = something?.foo()


// access it only if it is not null using safe operator,
// otherwise a default value using the elvis operator
val result3 = something?.foo() ?: differentValue


// null check it with `if` expression and then use the value,
// similar to result3 but for more complex cases harder to do in one expression
val result4 = if (something != null) {
something.foo()
} else {
...
differentValue
}


// null check it with `if` statement doing a different action
if (something != null) {
something.foo()
} else {
someOtherAction()
}

对于“Why does it work when null checked"阅读下面关于聪明的投的背景信息。

在带有Map的问题中你问题中的第二个案例,如果你作为开发人员确定结果永远不会是null,则使用!! sure操作符作为断言:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

或者在另一种情况下,当映射可以返回null但你可以提供默认值时,则Map本身具有getOrElse方法:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.getOrElse("z") { 0 } // provide default value in lambda
something.toLong() // now valid

背景信息:

注意: 在下面的例子中,我使用显式类型使行为清晰。使用类型推断,通常可以省略局部变量和私有成员的类型。

关于!! sure操作符的更多信息

!!操作符断言该值不是null或抛出NPE。这应该用于开发人员保证值永远不会是null的情况。可以把它看作是一个assert后跟聪明的演员

val possibleXyz: Xyz? = ...
// assert it is not null, but if it is throw an exception:
val surelyXyz: Xyz = possibleXyz!!
// same thing but access members after the assertion is made:
possibleXyz!!.foo()

阅读更多:!!确定操作符< / >


更多关于null检查和智能类型转换

如果你用null检查来保护对可空类型的访问,编译器会将语句体中的值聪明的演员为非空。在一些复杂的流程中,这是不可能发生的,但对于一般情况来说是可以的。

val possibleXyz: Xyz? = ...
if (possibleXyz != null) {
// allowed to reference members:
possiblyXyz.foo()
// or also assign as non-nullable type:
val surelyXyz: Xyz = possibleXyz
}

或者如果你对非空类型进行is检查:

if (possibleXyz is Xyz) {
// allowed to reference members:
possiblyXyz.foo()
}

对于'when'表达式也是安全强制转换:

when (possibleXyz) {
null -> doSomething()
else -> possibleXyz.foo()
}


// or


when (possibleXyz) {
is Xyz -> possibleXyz.foo()
is Alpha -> possibleXyz.dominate()
is Fish -> possibleXyz.swim()
}

有些东西不允许null检查聪明的演员以便以后使用该变量。上面的例子使用了一个局部变量,该变量在应用程序流中不可能发生突变,无论是val还是var,该变量都没有机会突变为null。但是,在编译器不能保证流分析的其他情况下,这将是一个错误:

var nullableInt: Int? = ...


public fun foo() {
if (nullableInt != null) {
// Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time"
val nonNullableInt: Int = nullableInt
}
}

变量nullableInt的生命周期是不完全可见的,可以从其他线程分配,null检查不能将聪明的演员转换为一个非空值。请参阅“安全呼叫”;下面的主题为解决方案。

另一种不能被聪明的演员信任不会发生变化的情况是具有自定义getter的对象上的val属性。在这种情况下,编译器不知道是什么改变了值,因此你会得到一个错误消息:

class MyThing {
val possibleXyz: Xyz?
get() { ... }
}


// now when referencing this class...


val thing = MyThing()
if (thing.possibleXyz != null) {
// error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter"
thing.possiblyXyz.foo()
}

阅读更多:检查条件中的null


关于?.安全调用操作符的更多信息

如果左边的值为空,则安全调用运算符返回null,否则继续计算右边的表达式。

val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable()
// "answer" will be null if any step of the chain is null
val answer = possibleXyz?.foo()?.goo()?.boo()

在另一个例子中,你想要迭代一个列表,但前提是它不是null且不是空的,安全调用操作符再次派上用场:

val things: List? = makeMeAListOrDont()
things?.forEach {
// this loops only if not null (due to safe call) nor empty (0 items loop 0 times):
}

在上面的一个例子中,我们有一个情况,我们做了if检查,但有机会另一个线程改变了值,因此没有聪明的演员。我们可以改变这个例子,使用安全调用操作符和let函数来解决这个问题:

var possibleXyz: Xyz? = 1


public fun foo() {
possibleXyz?.let { value ->
// only called if not null, and the value is captured by the lambda
val surelyXyz: Xyz = value
}
}

阅读更多:安全呼叫


关于?: Elvis操作符的更多信息

Elvis操作符允许你在操作符左边的表达式为null时提供一个替代值:

val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

它也有一些创造性的用途,例如,当某个东西是null时抛出异常:

val currentUser = session.user ?: throw Http401Error("Unauthorized")

或者提前从函数返回:

fun foo(key: String): Int {
val startingCode: String = codes.findKey(key) ?: return 0
// ...
return endingValue
}

阅读更多:猫王运算符


具有相关函数的空操作符

Kotlin stdlib有一系列函数,它们与上面提到的操作符一起工作得非常好。例如:

// use ?.let() to change a not null value, and ?: to provide a default
val something = possibleNull?.let { it.transform() } ?: defaultSomething


// use ?.apply() to operate further on a value that is not null
possibleNull?.apply {
func1()
func2()
}


// use .takeIf or .takeUnless to turn a value null if it meets a predicate
val something = name.takeIf { it.isNotBlank() } ?: defaultName


val something = name.takeUnless { it.isBlank() } ?: defaultName

相关的话题

在Kotlin中,大多数应用程序都试图避免null值,但这并不总是可行的。有时null是完全有意义的。以下是一些值得思考的指导原则:

  • 在某些情况下,它保证不同的返回类型,包括方法调用的状态和成功的结果。像结果这样的库给你一个成功或失败的结果类型,它也可以分支你的代码。Kotlin的Promises库Kovenant以promise的形式做同样的事情。

  • 对于作为返回类型的集合总是返回一个空集合而不是null,除非你需要第三种状态“不呈现”。Kotlin有诸如__ABC1或emptySet()这样的辅助函数来创建这些空值。

  • 当使用返回一个可空值的方法时,使用Elvis操作符提供一个默认值。对于Map,使用允许生成默认值的getOrElse()方法,而不是返回可空值的Map方法get()getOrPut()也是一样

  • 当从Java重写方法时,Kotlin不确定Java代码的可空性,如果你确定签名和功能应该是什么,你总是可以从你的重写中删除?可空性。因此,被重写的方法更加null安全。在Kotlin中实现Java接口也是一样,将可空性更改为您所知道的有效值。

  • 查看已经可以提供帮助的函数,例如String?.isNullOrEmpty()String?.isNullOrBlank(),它们可以安全地操作可空值并执行您所期望的操作。事实上,您可以添加自己的扩展来填补标准库中的任何空白。

  • 断言函数,如标准库中的checkNotNull()requireNotNull()

  • 帮助函数,如filterNotNull(),它从集合中删除空值,或listOfNotNull(),用于从可能的null值返回零或单项列表。

  • 还有一个安全(可空)强制转换运算符,它允许转换为非空类型,如果不可能,返回null。但是我没有一个有效的用例,上面提到的其他方法不能解决这个问题。

前面的答案很难做到,但这里有一个快速简单的方法:

val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo()

如果它真的永远不为空,异常就不会发生,但如果它真的为空,你就会看到哪里出了问题。

我想补充一点,现在它存在Konad库,用于解决更复杂的可空组合情况。下面是一个示例用法:

val foo: Int? = 1
val bar: String? = "2"
val baz: Float? = 3.0f


fun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt()


val result: Int? = ::useThem.curry()
.on(foo.maybe)
.on(bar.maybe)
.on(baz.maybe)
.nullable

如果你想让它为空,或者

val result: Result<Int> = ::useThem.curry()
.on(foo.ifNull("Foo should not be null"))
.on(bar.ifNull("Bar should not be null"))
.on(baz.ifNull("Baz should not be null"))
.result

如果你想积累误差。看到也许部分

接受的答案包含完整的细节,在这里我添加了摘要

如何调用null类型变量上的函数

val str: String? = "HELLO"


// 1. Safe call (?), makes sure you don't get NPE
val lowerCaseStr = str?.toLowerCase()   // same as str == null ? null : str.toLowerCase()


// 2. non-null asserted call (!!), only use if you are sure that value is non-null
val upperCaseStr = str!!.toUpperCase()  // same as str.toUpperCase() in java, NPE if str is null

如何将可为空的类型变量转换为不可为空的类型

假设您100%确定可空变量包含非空值

// use non-null assertion, will cause NPE if str is null
val nonNullableStr = str!!      // type of nonNullableStr is String(non-nullable)

为什么safe(?)或non-null(!!)断言不需要在null检查if block

如果编译器可以保证变量在检查和使用之间不会改变,那么它知道变量不可能为空,所以你可以这样做

if(str != null){
val upperCaseStr = str.toUpperCase()   // str can't possibly be null, no need of ? or !!
}