小编典典

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

all

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

假设我确定它确实是 never ,我如何在null不需要检查的情况下更改或处理一个值?例如,在这里我从一个我可以保证存在的映射中检索一个值,并且结果is
not 。但我有一个错误: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()认为该项目可能丢失并返回 type Int?。因此,强制值的类型不可为空的最佳方法是什么?

注意: 这个问题是作者有意编写和回答的,因此常见的 Kotlin 主题的惯用答案出现在 SO 中。还要澄清一些为 Kotlin 的 alpha
编写的非常古老的答案,这些答案对于当今的 Kotlin 并不准确。


阅读 88

收藏
2022-07-09

共1个答案

小编典典

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

在 Kotlin 中,您不能访问一个可为空的值,除非确定它不是null在条件中检查
null
),或者断言它肯定没有null使用!!确定运算符,使用?.安全调用访问它,或者最后给出可能null是使用?:Elvis
Operator
的默认值。

对于您的问题中的第一种情况,您 可以根据代码的意图使用其中一个选项,并且所有选项都是惯用的,但结果不同:

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
时它会起作用”,请阅读下面有关智能转换的背景信息。

对于您的问题 中的第二种情况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方法

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检查进行智能转换以供以后使用变量。上面的示例使用了一个局部变量,该变量绝不可能在应用程序流程中发生变异,无论valvar变量是否有机会变异为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检查不能智能转换为不可为空的值。有关解决方法,请参阅下面的“安全呼叫”主题。

智能强制转换不信任的另一种情况是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()
}

阅读更多: 检查条件中的空值


有关?.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检查,但有机会另一个线程改变了值,因此没有智能转换。我们可以将此示例更改为使用安全调用运算符以及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

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
}

阅读更多: 猫王操作员


具有相关功能的空运算符

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以 Promise的形式做同样的事情。

  • 作为返回类型的集合总是返回一个空集合而不是 a null,除非您需要“不存在”的第三种状态。Kotlin 具有辅助函数,例如创建这些空值的emptyList()或。emptySet()

  • 当使用返回可空值且您有默认值或替代值的方法时,请使用 Elvis 运算符提供默认值。在Map使用的情况下getOrElse(),允许生成默认值而不是返回可空值的Map方法。get()相同的getOrPut()

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

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

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

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

  • 还有一个安全(可为空)强制转换运算符,如果不可能,它允许强制转换为不可为空的类型返回 null。但是我没有一个有效的用例,上面提到的其他方法没有解决这个问题。

2022-07-09