Kotlin Vocabulary | 使用 Kotlin 中的扩展提升代码可读性

您有过想给某个类的 API 添加新的功能或属性吗?

通常您可以通过继承该类,或者创建一个新的函数,该函数接收该类的实例作为参数,从而解决这个问题。Java 编程语言通常使用 Utils 类来解决此类问题,但这样的方式并不支持代码自动补全,会让写出的代码比较难以查找,使用起来也不直观。虽然这两种方式都可以解决问题,但终究还是很难写出简洁易读的代码。

值得庆幸的是,Kotlin 带着 扩展函数和属性 来 “拯救” 我们了。通过它,您无需使用继承,或创建接收类实例的函数即可为某个类添加功能。同 Java 这类编程语言不同,Android Studio 的自动补全功能是支持 Kotlin 扩展的。扩展可以用于第三方代码库、Android SDK 以及用户自定义的类。

继续阅读,探索如何通过扩展来提升您的代码可读性。

扩展函数的使用

我们假设您有一个叫做 Dog 的类,它有 name、breed、age 三个属性。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

data class Dog(val name: String, val breed: String, val age: Int)

领养机构希望扩展 Dog 类,使其具有打印狗狗信息的功能,这样可以方便感兴趣的人来领养。为此我们实现一个扩展函数,方法同实现一个普通的函数是一样的,除了一点: 您需要在函数名前面加上要扩展的类名以及一个 “.” 符号。在函数体中,您可以使用 this 来引用接收者对象,在该函数作用域内能够访问到接收者所属类的全部成员对象。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun Dog.printDogInformation() {
  println("Meet ${this.name}, a ${this.age} year old ${this.breed}")
}

调用 printDogInformation() 方法就同调用其它 Dog 类中的函数一样。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog = Dog("Jen", "Pomeranian", 13)
  dog.printDogInformation()
}

从 Java 代码中调用扩展函数

扩展函数并不属于我们要扩展的类的一部分,因此当我们在 Java 语言中尝试调用该方法时,并不能在该类的其它方法中找到它。正如我们稍后所看到的,扩展会在其被定义的文件中反编译成静态方法,并接收一个我们要扩展的类的实例作为参数。以下就是在 Java 中调用 printDogInformation() 扩展函数的示例代码。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

DogExtensionKt.printDogInformation(dog);

为 nullable 类型定义扩展函数

您也可以为 nullable 类型定义扩展函数。与其在调用扩展函数之前进行 null 检查,我们可以直接为 nullable 类型定义扩展函数,让扩展函数本身包含对 null 的检查。以下就是为 nullable 类型定义扩展函数 printInformation() 的示例代码。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun Dog?.printInformation() {
  if (this == null){
    println("No dog found")
    return
  }
  println("Meet ${this.name} a ${this.age} year old ${this.breed}")
}

您可以发现,调用 printInformation() 函数时并不需要做 null 检查。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog : Dog? = null
  dog.printInformation() // prints "No dog found"
}

扩展属性的使用

作为领养机构,可能还想知道狗狗的年龄是否符合被领养的条件,因此我们实现了一个名为 isReadyToAdopt 的扩展属性,用于检查狗狗的年龄是否超过 1 岁。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

val Dog.isReadyToAdopt: Boolean
get() = this.age > 1

调用此属性就同调用 Dog 类中的其它属性一样。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog1 = Dog("Jen", "Pomeranian", 13)
  if(dog1.isReadyToAdopt){
    print("${dog1.name} is ready to be adopted")
  }
}

扩展函数中的复写

您并不能在扩展函数里复写类中现有的成员函数。如果您所定义的扩展函数同已有的成员函数签名一致,那么只有现有的成员函数会被正常调用,因为函数调用取决于变量声明时的静态类型,而不是存储在该变量中值的运行时类型。例如,您不能在 String 上扩展 toUppercase() 方法,但是您可以扩展一个名为 convertToUppercase() 的方法。

当您扩展了一个不属于您定义的类型,而该类型所在的代码库中存在一个同您的扩展具有相同签名的扩展函数,那么上述所说的这种行为就会显现出后果。在这种情况下,会调用代码库中的扩展函数,而您所得到的唯一信息是您所定义的扩展函数变成了一个未被使用的方法。

工作原理

我们可以在 Android Studio 中对 printDogInformation() 反编译,方法是在 Tools/Kotlin/Show Kotlin Bytecode 中点击 Decompile 按钮。以下是反编译 printDogInformation() 之后生成的代码:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

public static final void printDogInformation(@NotNull Dog $this$printDogInformation) {
  Intrinsics.checkParameterIsNotNull($this$printDogInformation, "$this$printDogInformation");
  String var1 = "Meet " + $this$printDogInformation.getName() + ", a " + $this$printDogInformation.getAge() + " year old " + $this$printDogInformation.getBreed();
  boolean var2 = false;
  System.out.println(var1);
}

实际上,扩展函数看起来只是普通的、接收一个类实例作为参数的静态函数,与接收类并没有任何其它联系。这就是为什么代码没有 Backing Fields 的原因——它们实际上并没有在类中插入任何成员。

总结

总的来说,扩展是一个很有用的工具。在使用扩展时需仔细思虑,请牢记以下提示,让您的代码更直观和易读。

提示:

  • 扩展是静态分发的;
  • 成员函数永远是 “赢家”;
  • 领养一只狗狗!

祝您编码愉快!