Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

安全调用和 Elvis 运算符

Kotlin 提供了方便的操作来处理可空性。

可空类型带来了许多限制。您不能简单地解引用可空类型的标识符:

// SafeCallsAndElvis/DereferenceNull.kt

fun main() {
  val s: String? = null
  // 不能编译:
  // s.length        // [1]
}

取消 [1] 的注释会产生编译时错误:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?.

安全调用将常规调用中的点(.)替换为问号和点(?.),中间没有空格。安全调用以确保不会抛出异常的方式访问可空类型的成员。它们仅在接收方不为 null 时执行操作:

// SafeCallsAndElvis/SafeOperation.kt
package safecalls
import atomictest.*

fun String.echo() {
  trace(toUpperCase())
  trace(this)
  trace(toLowerCase())
}

fun main() {
  val s1: String? = "Howdy!"
  s1?.echo()                  // [1]
  val s2: String? = null
  s2?.echo()                  // [2]
  trace eq """
    HOWDY!
    Howdy!
    howdy!
  """
}

[1] 行调用 echo() 并在 trace 中产生结果,而 [2] 行不执行任何操作,因为接收方 s2null

安全调用是一种捕获结果的简洁方式:

// SafeCallsAndElvis/SafeCall.kt
package safecalls
import atomictest.eq

fun checkLength(s: String?, expected: Int?) {
  val length1 =
    if (s != null) s.length else null  // [1]
  val length2 = s?.length              // [2]
  length1 eq expected
  length2 eq expected
}

fun main() {
  checkLength("abc", 3)
  checkLength(null, null)
}

[2] 行实现了与 [1] 行相同的效果。如果接收方不为 null,它会执行正常的访问(s.length)。如果接收方为 null,它不会执行 s.length 调用(这会导致异常),而是对表达式产生 null

如果您需要的不仅仅是 ?. 产生的 nullElvis 运算符 提供了一种替代方法。该运算符是一个问号后跟一个冒号(?:),中间没有空格。它是以音乐家 Elvis Presley 的表情符号命名的,也是 “else-if” 这个词的一种变种(听起来有点像 “Elvis”)。

许多编程语言都提供了与 Kotlin 的 Elvis 运算符执行相同操作的 null 合并运算符

如果 ?: 左侧的表达式不为 null,则该表达式成为结果。如果左侧表达式 null,则 ?: 右侧的表达式成为结果:

// SafeCallsAndElvis/ElvisOperator.kt
import atomictest.eq

fun main() {
  val s1: String? = "abc"
  (s1 ?: "---") eq "abc"

  val s2: String? = null
  (s2 ?: "---") eq "---"
}

s1 不为 null,因此 Elvis 运算符将 "abc" 作为结果。由于 s2null,Elvis 运算符产生备用结果 "---"

Elvis 运算符通常在安全调用之后使用,以生成有意义的值,而不是默认的 null,就像 [2] 中看到的那样:

// SafeCallsAndElvis/ElvisCall.kt
package safecalls
import atomictest.eq

fun checkLength(s: String?, expected: Int) {
  val length1 =
    if (s != null) s.length else 0    // [1]
  val length2 = s?.length ?: 0        // [2]
  length1 eq expected
  length2 eq expected
}

fun main() {
  checkLength("abc", 3)
  checkLength(null, 0)
}

这个 checkLength() 函数与上面的 SafeCall.kt 中的函数非常相似。expected 参数的类型现在是非可空的。[1][2] 在产生 null 代替的位置上产生了零。

当您使用安全调用来链接访问多个成员时,如果任何中间表达式为 null,结果将为 null

// SafeCallsAndElvis/ChainedCalls.kt
package safecalls
import atomictest.eq

class Person(
  val name: String,
  var friend: Person? = null
)

fun main() {
  val alice = Person("Alice")
  alice.friend?.friend?.name eq null   // [1]

  val bob = Person("Bob")
  val charlie = Person("Charlie", bob)
  bob.friend = charlie
  bob.friend?.friend?.name eq "Bob"    // [2]

  (alice.friend?.friend?.name
    ?: "Unknown") eq "Unknown"         // [3]
}

当您使用安全调用来链式访问多个成员时,如果任何中间表达式为 null,结果将为 null

  • [1] 属性 alice.friendnull,因此其他调用的返回值都是 null
  • [2] 所有中间调用都产生有意义的值。
  • [3] 安全调用链之后的 Elvis 运算符提供了一个备用值,如果任何中间元素为 null

练习和答案可以在 www.AtomicKotlin.com 找到。