首页 文章

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

提问于
浏览
128

如果我有一个可空类型 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() 
}

如果不确定 if ,我如何更改或处理值不是 null ,假设我确定它确实永远不会 null ?例如,这里我从 Map 中检索一个我可以保证存在的值,而 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? . 因此,强制值的类型不可为空的最佳方法是什么?

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

2 回答

  • 1

    首先,你应该阅读Kotlin的所有关于Null Safety的内容 .

    在Kotlin中,你不能在不确定它不是 nullChecking for null in conditions)的情况下访问可空值,或者断言它肯定不是 null 使用!! sure operator,用?. Safe Call访问它,或者最后使用 null 使用 null 使用默认值?: Elvis Operator .

    For your 1st case in your question 你有选择,取决于你将使用其中一个代码的意图,并且所有都是惯用的,但有不同的结果:

    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",请阅读smart casts下面的背景信息 .

    For your 2nd case in your questionMap 的问题中,如果您作为开发人员确定结果永远不会 null ,请使用 !! sure运算符作为断言:

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

    或者在另一种情况下,当 Map COULD返回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
    

    背景资料:

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

    更多关于!!确定的运营商

    !! 运算符声明该值不是 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'表达式也是如此:

    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为非可空值 . 有关解决方法,请参阅下面的"Safe Calls"主题 .

    另一个不能被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


    关于?:猫王操作员的更多信息

    Elvis运算符允许您在运算符左侧的表达式为 null 时提供替代值:

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

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

    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的Promises库叫做Kovenant以承诺的形式做同样的事情 .

    • for collections作为返回类型总是返回一个空集合而不是 null ,除非你需要第二个状态"not present" . 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 . 但是我没有一个有效的用例,这个用例没有通过上面提到的其他方法解决 .

  • 231

    之前的答案是一个难以遵循的行为,但这是一个快速简便的方法:

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

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

相关问题