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

时间:2015-12-28 18:13:07

标签: nullable kotlin non-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?"

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

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

如果不确定null,我该如何更改或处理某个值if,假设我确定它确实永远不会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?。因此,强制值的类型不可为空的最佳方法是什么?

注意: 此问题是由作者(Self-Answered Questions)故意编写和回答的,因此常见问题的Kotlin主题的惯用答案存在于SO中。还要澄清为Kotlin的alphas写的一些非常古老的答案,这些答案对于当前的Kotlin来说是不准确的。

4 个答案:

答案 0 :(得分:250)

首先,您应该阅读Kotlin中关于案件的Null Safety的所有内容。

在Kotlin中,如果不确定它不是nullChecking for null in conditions),或者使用!! 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() 
}

对于"为什么在检查空值时它起作用"请阅读以下smart casts

的背景信息 对于Map问题,

问题中的第二个案例,如果您作为开发人员确定结果永远不会是null,请使用!!确定运算符作为断言:

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

或者在另一种情况下,当地图可以返回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

背景资料:

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

有关!!肯定运营商

的更多信息

!!运算符声明该值不是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语句正文中的值不可为空。有一些复杂的流程,这是不可能的,但对于常见的情况工作正常。

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 (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为非可空值。请参阅"安全通话"以下主题为解决方法。

smart cast无法信任的另一种不变异的情况是具有自定义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()
}

了解详情:Checking for null in conditions

有关?.安全呼叫运营商

的更多信息

如果左边的值为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检查,但有可能另一个线程突变了值,因此没有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

有关?:猫王运营商

的更多信息

当操作符左侧的表达式为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 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非常有意义。一些指导思考:

  • 在某些情况下,它保证包含方法调用状态的不同返回类型以及成功时的结果。像Result这样的库可以为您提供成功或失败的结果类型,也可以为您的代码分支。 Kotlin的Promise图书馆Kovenant以承诺的形式做同样的事情。

  • 对于集合作为返回类型始终返回空集合而不是null,除非您需要第三个状态为"不存在"。 Kotlin有辅助函数,例如emptyList() or emptySet()来创建这些空值。

  • 当使用返回可以为您拥有默认值或替代值的可空值的方法时,请使用Elvis运算符提供默认值。如果Map使用getOrElse(),则允许生成默认值,而不是Map方法get(),后者返回可以为空的值。 getOrPut()

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

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

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

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

  • 还有Safe (nullable) cast operator允许转换为非可空类型,如果不可能则返回null。但是我没有一个有效的用例,并没有通过上面提到的其他方法解决。

答案 1 :(得分:2)

之前的回答是一个难以理解的行为,但这是一种快速简便的方法:

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

如果它真的永远不会为空,那么异常就不会发生,但是如果有的话,你会看到出了什么问题。

答案 2 :(得分: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

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

答案 3 :(得分: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)

为什么在 null 内不需要安全(?)或非空(!!)断言检查是否阻塞

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

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