【问题标题】:In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them在 Kotlin 中,处理可空值、引用或转换它们的惯用方式是什么
【发布时间】:2016-04-02 14:24:34
【问题描述】:

如果我有一个可空类型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 alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 并不准确。

【问题讨论】:

    标签: nullable kotlin non-nullable


    【解决方案1】:

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

    在 Kotlin 中,如果不能确定它不是 null (Checking 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() 
    }
    

    对于“为什么在检查 null 时它会起作用”,请阅读下面smart casts 上的背景信息。

    对于您的问题中的第二种情况,在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 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 语句主体内的值设为不可为空。有一些复杂的流程不会发生这种情况,但对于常见的情况可以正常工作。

    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变成一个不可为空的值。有关解决方法,请参阅下面的“安全呼叫”主题。

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


    有关?. Safe Call 运营商的更多信息

    如果左边的值为 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 Operator

    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
    }
    

    阅读更多:Elvis Operator


    具有相关功能的空运算符

    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() 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。但是我没有一个有效的用例,上面提到的其他方法都没有解决这个问题。

    【讨论】:

    • 变量和函数的命名很糟糕。
    • @Waldmann 随时编辑和清理这些内容,这很有帮助!
    【解决方案2】:

    前面的答案很难理解,但这里有一个快速简便的方法:

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

    如果它真的从不为空,则不会发生异常,但如果确实如此,您会看到哪里出了问题。

    【讨论】:

    • 验证某事:Xyz = createPossiblyNullXyz()!!当 createPossiblyNullXyz() 返回 null 时将抛出 NPE。它更简单,并且遵循处理您知道不为空的值的约定
    • 这个模式还是很有用的,尤其是对于请求验证。是的,如果它不允许为 null,您可以断言 !!,但您可能希望抛出一个更明确的异常,您的代码可以理解并为您格式化。 HandledException(message = "Email is required", status = 400) 例如。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

        猜你喜欢
        • 2014-12-08
        • 2019-04-06
        • 2017-06-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-04-17
        • 1970-01-01
        相关资源
        最近更新 更多