在Kotlin中,处理可空值的惯用方式是引用或转换它们?

204
如果我有一个可空类型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?"

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

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

如果我确定某个值绝不会为null,在不需要if检查的情况下如何更改或处理该值?例如,我从一个映射中检索一个值,我可以保证它存在且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?。因此,强制该值的类型不可为空的最佳方式是什么?

注意:本问题是由作者 (自我回答问题) 故意编写和回答的,以便SO中包含常见 Kotlin 主题的惯用回答。同时澄清一些针对 Kotlin Alpha 写的非常旧的答案,这些答案对于当前 Kotlin 不准确。

4个回答

334

首先,您应该阅读有关Kotlin中Null Safety的所有内容,该内容全面涵盖了各种情况。

在Kotlin中,您不能访问可空值而不确定它是否为null检查条件是否为null),或者使用!! sure operator断言它肯定不是null,使用?. Safe Call访问它,或者最后使用?: Elvis Operator给可能为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() 
}

针对“为什么进行空值检查后能够正常工作”,请阅读下面关于智能转换的背景信息。
针对您在问题中提到的第二种情况,如果您作为开发者确定结果永远不会为null,请使用!!安全操作符作为断言。
val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

在另一种情况下,当地图可能返回空值但您可以提供默认值时,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

背景信息:

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

更多关于!!操作符

!!操作符断言该值不是null,否则会抛出NPE异常。这应该在开发人员保证该值永远不会为null的情况下使用。可以将其视为一个智能转换后的断言。

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
    }
}

The lifecycle of the variable nullableInt is not completely visible and may be assigned from other threads, the null check cannot be smart cast into a non-nullable value. See the "Safe Calls" topic below for a workaround.
Another case that cannot be trusted by a smart cast to not mutate is a val property on an object that has a custom getter. In this case, the compiler has no visibility into what mutates the value and therefore you will get an error message.
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时返回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运算符

当运算符左侧的表达式为null时,Elvis运算符允许你提供一个替代值:

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
}

阅读更多:Elvis运算符


与相关函数一起使用的 Null 运算符

Kotlin 标准库提供了一系列与上述运算符非常配合的函数。例如:

// 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是完全合理的。以下是一些需要考虑的指南:

在某些情况下,需要不同的返回类型,包括方法调用的状态和成功时的结果。像Result这样的库可以给你一个成功或失败的结果类型,也可以分支你的代码。而Kotlin的Promises库Kovenant以promise的形式提供相同的功能。
对于集合作为返回类型,除非你需要第三种“不存在”的状态,否则总是返回一个空集合而不是null。 Kotlin有一些辅助函数,如emptyList()emptySet()来创建这些空值。
当使用返回可为空值的方法时,如果你有默认值或备选项,请使用Elvis运算符提供默认值。在Map的情况下,请使用getOrElse(),它允许生成默认值,而不是返回可为空值的Map方法get()。对于getOrPut()也是如此。
当覆盖Java中不确定Java代码的可空性的方法时,如果您确定签名和功能应该是什么,可以始终从覆盖中删除?可空性。因此,您的覆盖方法更加安全。实现Kotlin中的Java接口时也是如此,将可空性更改为您知道有效的内容。
查看已有的函数是否有帮助,例如String?.isNullOrEmpty()String?.isNullOrBlank(),它们可以安全地操作可为空的值并执行您所期望的操作。事实上,您可以添加自己的扩展来填补标准库中的任何空白。
在标准库中,有像checkNotNull()requireNotNull()这样的断言函数。
还有像filterNotNull()这样的辅助函数,它可以从集合中删除null,或者像listOfNotNull()这样的函数,可以从可能的null值返回零个或单个项目列表。
还有一个安全(可为空)转换运算符,如果不可能,允许将类型转换为非空类型返回null。但我没有一个有效的用例,不能通过上述方法解决。

4
之前的回答难以超越,但是这里有一种快速简单的方法:
val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo() 

如果它确实从未为null,那么异常就不会发生,但是如果它曾经为null,您将看到出了什么问题。


15
当createPossiblyNullXyz()返回null时,val something: Xyz = createPossiblyNullXyz()!!将抛出NPE。这样做更简单,并遵循处理已知不为null值的约定。 - Steven Waterman
5
这种模式仍然非常有用,特别是用于请求验证。如果不允许为空,您可以使用 !! 进行断言,但您可能希望抛出一个更明确的异常,以便您的代码可以理解并为您格式化。例如,HandledException(message = "Email is required", status = 400)。 - Guybrush

0

我想补充一下,现在有一个名为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

如果你想累积错误。请参见可能的章节


0

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

如何在可空类型的变量上调用函数

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)

为什么在空检查if块内不需要安全(?)或非空(!!)断言

如果编译器可以保证在检查和使用之间变量不会改变,那么它知道该变量不可能为空,因此您可以执行

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接