Kotlin Vocabulary | Kotlin 默认参数

Kotlin Vocabulary | Kotlin 默认参数

默认参数 是一个简短而易用的功能,它可以让您无需模版代码便可实现函数重载。和 Kotlin 所提供的许多其他功能一样,默认参数会给人一种魔法般的感觉。如果您想要知道其中的奥秘,请继续阅读,本文将会揭晓默认参数内部的工作原理。

基本用法

如果您需要重载一个函数,您可以使用默认参数,而不是将同一个函数实现许多次:

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

// 无需像下面这样实现:
fun play(toy: Toy){ ... }

fun play(){
    play(SqueakyToy)
}

// 使用默认参数:
 fun play(toy: Toy = SqueakyToy)

fun startPlaying() {
    play(toy = Stick)
    play() // toy = SqueakyToy
}

默认参数也可以应用于构造函数中:

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

class Doggo(
    val name: String,
    val rating: Int = 11
)

val goodDoggo = Doggo(name = "Tofu")
val veryGoodDoggo = Doggo(name = "Tofu", rating = 12)

与 Java 代码相互调用

默认情况下,Java 无法识别默认值重载:

<!-- Copyright 2019 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 -->
     
// kotlin
fun play(toy: Toy = SqueakyToy) {... }
// java
DoggoKt.play(DoggoKt.getSqueakyToy());
DoggoKt.play(); // error: Cannot resolve method 'play()'

您需要在 Kotlin 函数上使用 @JvmOverloads 注解,以指示编译器生成重载方法:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

@JvmOverloads
fun play(toy: Toy = SqueakyToy) {… }

内部实现

让我们通过反编译后的 Java 代码看看编译器为我们生成了什么。您可以在 Android Studio 中选择 Tools -> Kotlin -> Show Kotlin Bytecode,然后点击 Decompile 按钮:

函数

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

fun play(toy: Toy = SqueakyToy)
...
fun startPlaying() {
    play(toy = Stick)
    play() // toy = SqueakyToy
}

// 反编译出的 Java 代码
public static final void play(@NotNull Toy toy) {
   Intrinsics.checkNotNullParameter(toy, "toy");
}

// $FF: synthetic method
public static void play$default(Toy var0, int var1, Object var2) {
   if ((var1 & 1) != 0) {
      var0 = SqueakyToy;
   }

   play(var0);
}

public static final void startPlaying() {
   play(Stick);
   play$default((Toy)null, 1, (Object)null);
}

我们可以看到,编译器生成了两个函数:

  • play —— 该函数有一个参数: Toy,它会在没有使用默认参数时被调用。

  • play$default 一个合成方法 —— 它有三个参数: ToyintObject。只要是使用了默认参数就会被调用。三个参数中的 Object 会一直是 null,但是 int 的值产生了变化,下面让我们来看看为什么。

int 参数

play$default 函数中 int 参数的值是基于传入的有默认参数的参数数量和其索引计算的。根据这一参数的值,Kotlin 编译器可以知道在调用 play 函数时使用哪个参数。

在我们的 play() 函数的示例代码中,索引位置为 0 的参数使用了默认参数。所以 play$default 在调用时传入的 int 参数为 int var1 = 2⁰:

play$default((Toy)null, 1, (Object)null);

这样一来,play$default 的实现便可以知道 var0 的值应当被替换为默认值。

为了进一步了解 int 参数的行为,我们来观察一个更为复杂的例子。让我们扩展 play 函数,并在调用时使用 doggo 和 toy 的默认参数:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
   
fun play(doggo: Doggo = goodDoggo, doggo2: Doggo = veryGoodDoggo, toy: Toy = SqueakyToy) {...}

fun startPlaying() {
    play2(doggo2 = myDoggo)
}

让我们来看看反编译后的代码中发生了什么:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
   
public static final void play(@NotNull Doggo doggo, @NotNull Doggo doggo2, @NotNull Toy toy) {
...
 }

// $FF: synthetic method
public static void play$default(Doggo var0, Doggo var1, Toy var2, int var3, Object var4) {
  if ((var3 & 1) != 0) {
     var0 = goodDoggo;
  }

  if ((var3 & 2) != 0) {
     var1 = veryGoodDoggo;
  }

  if ((var3 & 4) != 0) {
     var2 = SqueakyToy;
  }

  play(var0, var1, var2);
}

public static final void startPlaying() {
    play2$default((Doggo)null, myDoggo, (Toy)null, 5, (Object)null);
 }

我们可以看到此时 int 参数的值为 5,它计算的原理为: 位于 0 和 2 的参数使用了默认参数,所以 var3 = 2⁰ + 2² = 5。使用 按位与操作 对参数进行如下计算:

  • var3 & 1 != 0true 所以 var0 = goodDoggo
  • var3 & 2 != 0false 所以 var1 没有被替换
  • var3 & 4 != 0true 所以 var2 = SqueakyToy

通过对 var3 应用位掩码,编译器可以计算出哪个参数应当被替换为默认值。

Object 参数

您也许会注意到,在上面的例子中 Object 参数的值始终为 null,但在 play$default 函数中从未被用到过。该参数与支持重载函数中的默认值有关。

默认参数与继承

当我们想要覆盖某个使用了默认参数的函数时会发生什么呢?

让我们修改上面的示例并:

  • play 函数改为 Doggo 类型的 open 函数,并将 Doggo 改为 open 类型。
  • 创建一个新的类型: PlayfulDoggo,该类型继承 Doggo 并覆盖 play 函数。

当我们尝试在 PlayfulDoggo.play 函数中设置默认值时,会发现这一操作不被允许: 不能为被覆盖的函数的参数设置默认值

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

open class Doggo(
    val name: String,
    val rating: Int = 11
) {
    open fun play(toy: Toy = SqueakyToy) {...}
}

class PlayfulDoggo(val playfulness: Int, name: String, rating: Int) : Doggo(name, rating) {
    // 错误:不能为被覆盖的函数的参数设置默认值
    override fun play(toy: Toy = Stick) { }

如果我们移除覆盖操作符 override 并检查反编译的代码,PlayfulDoggo.play() 函数会变得如下列代码这样:

public void play(@NotNull Toy toy) {...  }

// $FF: synthetic method
public static void play$default(Doggo var0, Toy var1, int var2, Object var3) {
  if (var3 != null) {
     throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: play");
  } else {
     if ((var2 & 1) != 0) {
        var1 = DoggoKt.getSqueakyToy();
     }

     var0.play(var1);
  }
}

这是否意味着未来会支持使用默认参数进行 super 调用?我们拭目以待。

构造函数

对于构造函数,反编译后的代码只有一处不同:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */
   
// kotlin 声明
class Doggo(
    val name: String,
    val rating: Int = 11
)

// 反编译后的 Java 代码
public final class Doggo {
   ...

   public Doggo(@NotNull String name, int rating) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.rating = rating;
   }

   // $FF: synthetic method
   public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 2) != 0) {
         var2 = 11;
      }

      this(var1, var2);
   }

构造函数同样会创建一个合成方法,但是它在函数中使用了一个空的 DefaultConstructorMarker 对象而不是 Object:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

// kotlin
val goodDoggo = Doggo("Tofu")

// 反编译后的 Java 代码
Doggo goodDoggo = new Doggo("Tofu", 0, 2, (DefaultConstructorMarker)null);

就像主构造函数一样,拥有默认参数的次级构造函数也会生成一个使用 DefaultConstructorMarker 的合成方法:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

// kotlin
class Doggo(
    val name: String,
    val rating: Int = 11
) {
    constructor(name: String, rating: Int, lazy: Boolean = true)    
}

//反编译后的 Java 代码
public final class Doggo {
   ...
   public Doggo(@NotNull String name, int rating) {
      ...
   }

   // $FF: synthetic method
   public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 2) != 0) {
         var2 = 11;
      }

      this(var1, var2);
   }

   public Doggo(@NotNull String name, int rating, boolean lazy) {
      ...
   }

   // $FF: synthetic method
   public Doggo(String var1, int var2, boolean var3, int var4, DefaultConstructorMarker var5) {
      if ((var4 & 4) != 0) {
         var3 = true;
      }

      this(var1, var2, var3);
   }
}

总结

默认参数简单易用,它帮助我们减少了大量处理方法重载所需的模版代码,并允许我们为参数设置默认值。如同许多其他 Kotlin 关键字一样,我们可以通过观察编译器所生成的代码来了解其背后的原理。如果您想要了解更多,请参阅我们 Kotlin Vocabulary 系列 的其他文章。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io WebMaster | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.