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

In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them

如果我有一个可为 null 的类型 Xyz?,我想引用它或将其转换为一个不可为 null 的类型 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() 
}

如何在不需要 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()认为可能是物品丢失,returns输入Int?。因此,强制值的类型不可为空的最佳方法是什么?

注意: 这个问题是作者(Self-Answered Questions)特意写下并回答的,以便对常见的 Kotlin 主题进行地道的回答存在于 SO 中。还要澄清一些为 Kotlin alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。

首先,您应该阅读有关 Null Safety in Kotlin 的所有内容,其中涵盖了所有案例。

在 Kotlin 中,您不能在不确定它不是 nullChecking for null in conditions), or asserting that it is surely not null using the !! sure operator, accessing it with a ?. Safe Call, or lastly giving something that is possibly null a default value using the ?: 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 时它起作用”请阅读下面关于 smart casts 的背景信息。

对于你问题中的第二种情况,如果你作为开发人员确定结果永远不会是 null,使用 !! 确定运算符作为断言:

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

或者在另一种情况下,当映射可以 return 为 null 但您可以提供默认值时,则 Map 本身具有 getOrElse method:

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 的情况下使用。将其视为后跟 smart cast.

的断言
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()

阅读更多:!! Sure Operator


有关 null 检查和智能转换的更多信息

如果您使用 null 检查保护对可空类型的访问,编译器将 smart cast 语句主体中的值设为 non-nullable。有一些复杂的流程是不可能发生这种情况的,但对于常见情况来说效果很好。

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

或者如果您执行 is 检查 non-nullable 类型:

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 检查到 smart cast 以便以后使用变量。上面的示例使用了一个局部变量,该变量绝不会在应用程序的流程中发生变异,无论是 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检查不能smart cast变成non-nullable值。请参阅下面的“安全呼叫”主题以获取解决方法。

另一种 smart cast 不信任的情况是 val 属性 在具有自定义 getter 的对象上。在这种情况下,编译器看不到是什么改变了值,因此您将收到一条错误消息:

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

阅读更多:Checking for null in conditions


有关 ?. 安全呼叫运营商的更多信息

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

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 检查,但有机会另一个线程改变了值,因此没有 smart cast。我们可以更改此示例以使用安全调用运算符以及 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
    }
}

阅读更多:Safe Calls


有关 ?: 猫王运算符的更多信息

Elvis 运算符允许您在运算符左侧的表达式为 null:

时提供替代值
val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

它也有一些创造性的用途,例如在 null:

时抛出异常
val currentUser = session.user ?: throw Http401Error("Unauthorized")

或 return 从一个函数开始:

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

阅读更多:Elvis Operator


具有相关功能的空运算符

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 非常有意义。需要考虑的一些准则:

  • 在某些情况下,它保证了不同的 return 类型,包括方法调用的状态和成功时的结果。像 Result give you a success or failure result type that can also branch your code. And the Promises library for Kotlin called Kovenant 这样的库以 promises 的形式做同样的事情。

  • 对于 return 类型的集合总是 return 空集合而不是 null,除非你需要第三种状态“不存在”。 Kotlin 有辅助函数,例如 emptyList() or emptySet() 来创建这些空值。

  • 当使用 return 具有默认值或替代值的可空值的方法时,请使用 Elvis 运算符提供默认值。在 Map 的情况下使用 getOrElse() which allows a default value to be generated instead of Map method get() which returns a nullable value. Same for getOrPut()

  • 当覆盖 Java 的方法时,Kotlin 不确定 Java 代码的可空性,您始终可以从您的代码中删除 ? 可空性如果您确定签名和功能应该是什么,则覆盖。因此,您的覆盖方法更 null 安全。与在 Kotlin 中实现 Java 接口相同,将可空性更改为您所知道的有效。

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

  • 断言函数类似于标准库中的 checkNotNull() and requireNotNull()

  • helper 功能类似于 filterNotNull() which remove nulls from collections, or listOfNotNull(),用于从可能的 null 值 returning 零项或单个项列表。

  • 还有一个 Safe (nullable) cast operator 允许转换为 non-nullable 类型 return null 如果不可能。但是我没有一个有效的用例,上面提到的其他方法没有解决这个问题。

之前的答案很难遵循,但这里有一个快速简便的方法:

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

如果它真的永远不会为 null,则不会发生异常,但如果确实为 null,您就会看到哪里出了问题。

我想补充一点,现在存在 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

如果你想累积错误。参见 maybe section

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

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

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)

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

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

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