短くて使いやすいデフォルト引数 を利用すると、ボイラープレートを書くことなく関数のオーバーロードを実現できます。多くの Kotlin の機能と同じように、この機能も魔法のように便利です。その秘密を知りたいと思いませんか?この記事では、デフォルト引数の内部の仕組みを紹介します。
基本的な使用方法
関数のオーバーロードが必要な場合、同じ関数を複数回実装する代わりに、デフォルト引数を使うことができます。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// instead of:
fun play(toy: Toy){ ... }
fun play(){
play(SqueakyToy)
}
// use default arguments:
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 コードを見てみましょう。[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
}
// decompiled Java code
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);
}
コンパイラが 2 つの関数を生成していることがわかります。
play
— 1 つのパラメータ Toy
を受け取り、デフォルト引数が使われない場合に呼び出されます。合成メソッド play$default
— 3 つのパラメータ Toy
、int
、Object
を受け取ります。デフォルト引数が使われたときに呼び出されます。Object
パラメータは常に null
ですが、int の値は異なります。どう違うのかを見てみましょう。
int パラメータ
play$default
の int パラメータの値は、デフォルト引数が渡された引数の数と、そのインデックスに基づいて計算されます。Kotlin コンパイラは、どのパラメータを使って play 関数を呼び出すかを、このパラメータの値に基づいて判断します。
この例の play()
の呼び出しでは、インデックス 0 の引数がデフォルト引数を使っています。そのため、int var1 = 2⁰
を使って play$default
を呼び出します。
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 != 0
は true
なので、var0 = goodDoggo
var3 & 2 != 0
は false
なので、var1
は置換されないvar3 & 4 != 0
は true
なので、var2 = SqueakyToy
コンパイラは、var3
に適用されたビットマスクから、どのパラメータをデフォルト値と置き換えるかを計算できます。
Object パラメータ
上の例で、Object
パラメータの値が常に null になっていたことに気づいたかもしれません。実際に、play$default
関数ではこの値が使われることはありません。このパラメータは、オーバーライドする関数でデフォルト値をサポートするために使用されています。
デフォルト引数と継承
デフォルト引数がある関数をオーバーライドすると、何が起きるでしょうか。
先ほどの例を次のように変更してみましょう。
play
を Doggo
の Open
関数にし、Doggo
を Open
クラスにする。Doggo
を拡張した PlayfulDoggo
クラスを新しく作成し、play
をオーバーライドする
PlayfulDoggo.play にデフォルト値を設定しようとしても、次のように表示され、許可されません。An overriding function is not allowed to specify default values for its parameters (オーバーライド関数では、パラメータへのデフォルト値の指定は許可されていません)
/* 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) {
// error: An overriding function is not allowed to specify default values for its parameters
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 の呼び出しが将来的にサポートされるという意味なのでしょうか。この点については、成り行きを見守るしかありません。
コンストラクタ
コンストラクタでは、逆コンパイルした Java コードに 1 つだけ違う点があります。以下をご覧ください。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// kotlin declaration
class Doggo(
val name: String,
val rating: Int = 11
)
// decompiled Java code
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);
}
コンストラクタでも合成メソッドが作成されていますが、関数で使われていた Object
ではなく、DefaultConstructorMarker
が null
で呼び出されています。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// kotlin
val goodDoggo = Doggo("Tofu")
// decompiled Java code
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)
}
// decompiled Java code
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 の投稿 を確認してみてください。
この記事は Florina Muntenescu による Android Developers - Medium の記事 "Don’t argue with default arguments " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
短くて使いやすいデフォルト引数 を利用すると、ボイラープレートを書くことなく関数のオーバーロードを実現できます。多くの Kotlin の機能と同じように、この機能も魔法のように便利です。その秘密を知りたいと思いませんか?この記事では、デフォルト引数の内部の仕組みを紹介します。
基本的な使用方法
関数のオーバーロードが必要な場合、同じ関数を複数回実装する代わりに、デフォルト引数を使うことができます。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// instead of:
fun play(toy: Toy){ ... }
fun play(){
play(SqueakyToy)
}
// use default arguments:
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 コードを見てみましょう。[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
}
// decompiled Java code
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);
}
コンパイラが 2 つの関数を生成していることがわかります。
play
— 1 つのパラメータ Toy
を受け取り、デフォルト引数が使われない場合に呼び出されます。合成メソッド play$default
— 3 つのパラメータ Toy
、int
、Object
を受け取ります。デフォルト引数が使われたときに呼び出されます。Object
パラメータは常に null
ですが、int の値は異なります。どう違うのかを見てみましょう。
int パラメータ
play$default
の int パラメータの値は、デフォルト引数が渡された引数の数と、そのインデックスに基づいて計算されます。Kotlin コンパイラは、どのパラメータを使って play 関数を呼び出すかを、このパラメータの値に基づいて判断します。
この例の play()
の呼び出しでは、インデックス 0 の引数がデフォルト引数を使っています。そのため、int var1 = 2⁰
を使って play$default
を呼び出します。
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 != 0
は true
なので、var0 = goodDoggo
var3 & 2 != 0
は false
なので、var1
は置換されないvar3 & 4 != 0
は true
なので、var2 = SqueakyToy
コンパイラは、var3
に適用されたビットマスクから、どのパラメータをデフォルト値と置き換えるかを計算できます。
Object パラメータ
上の例で、Object
パラメータの値が常に null になっていたことに気づいたかもしれません。実際に、play$default
関数ではこの値が使われることはありません。このパラメータは、オーバーライドする関数でデフォルト値をサポートするために使用されています。
デフォルト引数と継承
デフォルト引数がある関数をオーバーライドすると、何が起きるでしょうか。
先ほどの例を次のように変更してみましょう。
play
を Doggo
の Open
関数にし、Doggo
を Open
クラスにする。Doggo
を拡張した PlayfulDoggo
クラスを新しく作成し、play
をオーバーライドする
PlayfulDoggo.play にデフォルト値を設定しようとしても、次のように表示され、許可されません。An overriding function is not allowed to specify default values for its parameters (オーバーライド関数では、パラメータへのデフォルト値の指定は許可されていません)
/* 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) {
// error: An overriding function is not allowed to specify default values for its parameters
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 の呼び出しが将来的にサポートされるという意味なのでしょうか。この点については、成り行きを見守るしかありません。
コンストラクタ
コンストラクタでは、逆コンパイルした Java コードに 1 つだけ違う点があります。以下をご覧ください。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// kotlin declaration
class Doggo(
val name: String,
val rating: Int = 11
)
// decompiled Java code
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);
}
コンストラクタでも合成メソッドが作成されていますが、関数で使われていた Object
ではなく、DefaultConstructorMarker
が null
で呼び出されています。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// kotlin
val goodDoggo = Doggo("Tofu")
// decompiled Java code
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)
}
// decompiled Java code
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 の投稿 を確認してみてください。