美文网首页Android开发者
Kotlin Vocabulary | Kotlin 默认参数

Kotlin Vocabulary | Kotlin 默认参数

作者: 谷歌开发者 | 来源:发表于2021-01-30 21:42 被阅读0次
    image

    默认参数 是一个简短而易用的功能,它可以让您无需模版代码便可实现函数重载。和 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 系列 的其他文章。

    相关文章

      网友评论

        本文标题:Kotlin Vocabulary | Kotlin 默认参数

        本文链接:https://www.haomeiwen.com/subject/bqoktltx.html