ユーザーは皆さんのアプリにシームレスな体験を期待しています。アプリがクラッシュすれば、低評価のレビューやアンインストールが増え、ブランドにダメージを与えてしまうでしょう。その一方で、コミュニティの皆さんとの対話の中で、Kotlin を採用する主な理由の 1 つはコードの高い安全性であるということをよく耳にします。実際に何社かのパートナーは、Kotlin を使ってコードの安定性を改善しています。この投稿では、その方法をいくつか紹介するとともに、Google Play ストアの統計結果にも注目し、Kotlin とクラッシュ数との間に相関関係があるかどうかを確認してみたいと思います。
アプリの品質が影響するのは、ユーザー エクスペリエンスだけではありません。クラッシュ多いと、他のいくつかの要素にも悪影響が生じます。
Kotlin で構築したアプリはクラッシュする可能性が 20% 低い
Google Play のトップ 1,000 アプリを調査してみたところ、Kotlin を使っているアプリは、それ以外のアプリよりもユーザー 1 人あたりのクラッシュ発生率が 20% 低いことがわかりました。
その具体例として、コードの 74% が Kotlin である Swiggy のエンジニアリング チームは、新機能の開発を Kotlin に移行して以来、クラッシュを 50% 減らしました。こういった結果を Kotlin はどのように実現しているのか見ていきましょう。
Google Play でのクラッシュの原因ナンバーワンは NullPointerException です。Google Home チームは、2018 年よりすべての新機能を Kotlin で書いています。その結果、1 年前と比べて null ポインタによるクラッシュが 33% 減少しました。
Google Home が NullPointerException を 33% 削減
NullPointerException を避けるには、メソッドを呼び出したりメンバーにアクセスしたりする前に、扱っているオブジェクトの参照が null でないことを確認しなければなりません。Kotlin では、null 可能性が型システムの一部として組み込まれています。たとえば、変数は最初から null 可能か null 不可能かを宣言する必要があります。null 可能性を型システムの一部として組み込むことで、コードベースについての自分の記憶や知識、またコンパイル時警告(フィールドやパラメータに @Nullable アノテーションを付けた場合)に頼る必要はなくなります。null 可能性を強制することで、単なる警告ではなく、コンパイル時にエラーが発生します。null 可能性を扱う方法については、こちらのページをご覧ください。
NullPointerException
@Nullable
私たちデベロッパーは、気づかないうちにアプリにたくさんの問題を紛れ込ませています。そのほとんどはあまりに軽微で、調査するのは難しいかもしれません。そのような問題のうち、Kotlin を使うことで回避できるものをいくつか紹介しましょう。
hashCode()
2 つのオブジェクトが等しい場合、それらのハッシュコードも同じである必要があります。しかし、どちらかのメソッドを実装し忘れたり、クラスに新しいプロパティを追加したときに更新し忘れてしまったりすることがよくあります。データを保持するためだけのクラスを扱う場合は、Kotlin のデータクラスを使うようにしましょう。データクラスを使うと、コンパイラが hashCode() と equals() を生成してくれるので、クラスのプロパティを変更すると自動的に更新されます。
equals()
2 つのオブジェクトは構造が等しい(中身が同じ)のでしょうか。それとも、参照が等しい(ポインタが同じ)のでしょうか。Java プログラミング言語では、プリミティブには常に == を使います。そのため、実際には構造が等しいかどうかを確認(equals() を呼び出してチェック)したいのに、オブジェクトにも ==(参照が等しい)を使ってしまうというのがよくある誤りです。まず、Kotlin にはプリミティブ型はなく、Int や String などのクラスを使います。つまり、すべてがオブジェクトなので、オブジェクトとプリミティブ型の区別を気にする必要はありません。次に、Kotlin では == は構造が等しい、=== は参照が等しいと定義されています。したがって、誤って参照が等しいかどうかを確認してしまうことはありません。
==
Int
String
===
enum を扱う場合、通常はすべての可能なケースを記述しなければなりません。そのため、switch や if else チェーンを使うことになります。enum を修正して新しい値を追加する場合、enum を使っている各コード スニペットを手動でチェックし、新しいケースに対応しているかどうかを確認する必要があります。しかし、これはエラーにつながりがちです。Kotlin では、when を式として使うと、この確認をコンパイラに任せることができます。すべての可能な分岐に対応していない場合、コンパイラ エラーが発生します。
switch
if else
ユーザーやブランドにとって、アプリの安定性は重要です。Kotlin を使い始めてクラッシュ率を減らし、ユーザーの満足度を高めてアプリの高評価を守り、ユーザーの維持や獲得につなげましょう。
詳しくは、Kotlin でより優れたアプリを作成するためにできることをお読みください。また、ケーススタディをご覧いただき、Kotlin のメリットをご確認ください。世界でデベロッパーに広く使われている言語の 1 つである Kotlin を使ってみたい方は、入門ページをご覧ください。
この記事は 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 -->
// 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)
class Doggo(
val name: String,
val rating: Int = 11
)
val goodDoggo = Doggo(name = "Tofu")
val veryGoodDoggo = Doggo(name = "Tofu", rating = 12)
デフォルトでは、Java はデフォルト値のオーバーロードを認識しません。
<!-- Copyright 2019 Google LLC. SPDX-License-Identifier: Apache-2.0 --> // kotlinfun play(toy: Toy = SqueakyToy) {... }// javaDoggoKt.play(DoggoKt.getSqueakyToy());DoggoKt.play(); // error: Cannot resolve method 'play()'
// kotlin
fun play(toy: Toy = SqueakyToy) {... }
// java
DoggoKt.play(DoggoKt.getSqueakyToy());
DoggoKt.play(); // error: Cannot resolve method 'play()'
コンパイラにオーバーロード メソッドを生成するよう指示するには、Kotlin 関数に @JvmOverloads アノテーションを追加します。
@JvmOverloads
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */@JvmOverloadsfun play(toy: Toy = SqueakyToy) {… }
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun play(toy: Toy = SqueakyToy) {… }
コンパイラが生成した内容を確認するため、逆コンパイルした Java コードを見てみましょう。[Tools] -> [Kotlin] -> [Show Kotlin Bytecode] を選択し、[Decompile] ボタンを押します。
[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 codepublic static final void play(@NotNull Toy toy) { Intrinsics.checkNotNullParameter(toy, "toy");}// $FF: synthetic methodpublic 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);}
...
// 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
Toy
play$default
int
Object
null
play$default の int パラメータの値は、デフォルト引数が渡された引数の数と、そのインデックスに基づいて計算されます。Kotlin コンパイラは、どのパラメータを使って play 関数を呼び出すかを、このパラメータの値に基づいて判断します。
この例の play() の呼び出しでは、インデックス 0 の引数がデフォルト引数を使っています。そのため、int var1 = 2⁰ を使って play$default を呼び出します。
play()
int var1 = 2⁰
これで、play$default の実装は、var0 の値をデフォルト値で置き換えなければならないことを認識できます。
var0
この int パラメータの動作を確認するため、もう少し複雑な例を見てみましょう。play 関数を拡張し、この関数を呼び出す際に doggo と toy をデフォルト引数として使うようにします。
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)}
fun play(doggo: Doggo = goodDoggo, doggo2: Doggo = veryGoodDoggo, toy: Toy = SqueakyToy) {...}
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 methodpublic 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); }
public static final void play(@NotNull Doggo doggo, @NotNull Doggo doggo2, @NotNull Toy toy) {
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);
play2$default((Doggo)null, myDoggo, (Toy)null, 5, (Object)null);
int パラメータが 5 になりました。この計算の仕組みは次のようになります。0 番目と 2 番目のパラメータでデフォルト引数が使われているので、var3 = 2⁰ + 2² = 5 となります。パラメータは、ビット単位の & 演算を使って次のように評価されます。
var3 = 2⁰ + 2² = 5
var3 & 1 != 0
true
var0 = goodDoggo
var3 & 2 != 0
false
var1
var3 & 4 != 0
var2 = SqueakyToy
コンパイラは、var3 に適用されたビットマスクから、どのパラメータをデフォルト値と置き換えるかを計算できます。
var3
上の例で、Object パラメータの値が常に null になっていたことに気づいたかもしれません。実際に、play$default 関数ではこの値が使われることはありません。このパラメータは、オーバーライドする関数でデフォルト値をサポートするために使用されています。
デフォルト引数がある関数をオーバーライドすると、何が起きるでしょうか。
先ほどの例を次のように変更してみましょう。
Doggo
Open
PlayfulDoggo
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) { }}
open class Doggo(
) {
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() は次のようになっています。
override
PlayfulDoggo.play()
public void play(@NotNull Toy toy) {... }// $FF: synthetic methodpublic 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); }}
public void play(@NotNull Toy toy) {... }
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 declarationclass Doggo( val name: String, val rating: Int = 11)// decompiled Java codepublic 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); }
// kotlin declaration
public final class Doggo {
public Doggo(@NotNull String name, int rating) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.rating = rating;
public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) {
var2 = 11;
this(var1, var2);
コンストラクタでも合成メソッドが作成されていますが、関数で使われていた Object ではなく、DefaultConstructorMarker が null で呼び出されています。
DefaultConstructorMarker
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */// kotlinval goodDoggo = Doggo("Tofu")// decompiled Java codeDoggo goodDoggo = new Doggo("Tofu", 0, 2, (DefaultConstructorMarker)null);
val goodDoggo = Doggo("Tofu")
Doggo goodDoggo = new Doggo("Tofu", 0, 2, (DefaultConstructorMarker)null);
デフォルト引数があるセカンダリ コンストラクタでも、プライマリ コンストラクタと同じように DefaultConstructorMarker を使った別の合成メソッドが生成されます。
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */// kotlinclass Doggo( val name: String, val rating: Int = 11) { constructor(name: String, rating: Int, lazy: Boolean = true) }// decompiled Java codepublic 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); }}
constructor(name: String, rating: Int, lazy: Boolean = true)
public Doggo(@NotNull String name, int rating, boolean lazy) {
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 の投稿を確認してみてください。
仕事を完了する方法の 1 つは、その仕事を他者に委譲することです。皆さんの仕事を友だちに委譲することを言っているわけではありません。今回のテーマは、あるオブジェクトから別のオブジェクトに委譲することです。
ソフトウェアの世界では、委譲という考え方は新しいものではありません。委譲はデザイン パターンの 1 つで、あるオブジェクトが デリゲートと呼ばれるヘルパー オブジェクトに委譲することでリクエストを処理することを指します。デリゲートの役割は、元のオブジェクトに代わってリクエストを処理し、その結果を元のオブジェクトが利用できるようにすることです。
Kotlin はクラス委譲とプロパティ委譲をサポートしているので、委譲を簡単に扱えます。さらに、いくつかの独自の組み込みデリゲートも提供しています。
最後に削除された項目を復元できる ArrayList を使うとしましょう。基本的に、必要なのは同じ ArrayList の機能だけですが、最後に削除された項目への参照が必要です。
ArrayList
これを実現する方法の 1 つは、ArrayList クラスを拡張することです。この新しいクラスは、MutableList インターフェースの実装ではなく ArrayList の具象クラスを拡張したものなので、ArrayList の具象クラスの実装と強く結合されることになります。
MutableList
MutableList の実装で remove() 関数をオーバーライドし、削除した項目の参照を保持できるようにしたうえで、その他の空の実装を他のオブジェクトに委譲したいと思ったことはありませんか?Kotlin では、これを実現する方法が提供されています。具体的には、内部 ArrayList インスタンスに作業の大半を委譲し、その動作をカスタマイズできます。これを行うため、Kotlin には新しいキーワード by が導入されています。
remove()
by
では、クラス委譲の仕組みを確認してみましょう。by キーワードを使うと、Kotlin は innerList インスタンスをデリゲートとして使用するコードを自動的に生成します。
innerList
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->class ListWithTrash <T>( private val innerList: MutableList<T> = ArrayList<T>()) : MutableCollection<T> by innerList { var deletedItem : T? = null override fun remove(element: T): Boolean { deletedItem = element return innerList.remove(element) } fun recover(): T? { return deletedItem }}
class ListWithTrash <T>(
private val innerList: MutableList<T> = ArrayList<T>()
) : MutableCollection<T> by innerList {
var deletedItem : T? = null
override fun remove(element: T): Boolean {
deletedItem = element
return innerList.remove(element)
fun recover(): T? {
return deletedItem
by キーワードは、MutableList インターフェースの機能を innerList という名前の内部 ArrayList インスタンスに委譲するよう Kotlin に伝えます。内部 ArrayList オブジェクトに直接橋渡しするメソッドが提供されるので、ListWithTrash は MutableList インターフェースのすべての機能をサポートします。さらに、独自の動作を追加することもできるようになります。
ListWithTrash
動作の仕組みを確認してみましょう。ListWithTrash のバイトコードを逆コンパイルした Java コードを見ると、Kotlin コンパイラが実際にラッパー関数を作成していることを確認できます。このラッパー関数が、内部 ArrayList オブジェクトの対応する関数を呼び出していることもわかります。
public final class ListWithTrash implements Collection, KMutableCollection { @Nullable private Object deletedItem; private final List innerList; @Nullable public final Object getDeletedItem() { return this.deletedItem; } public final void setDeletedItem(@Nullable Object var1) { this.deletedItem = var1; } public boolean remove(Object element) { this.deletedItem = element; return this.innerList.remove(element); } @Nullable public final Object recover() { return this.deletedItem; } public ListWithTrash() { this((List)null, 1, (DefaultConstructorMarker)null); } public int getSize() { return this.innerList.size(); } // $FF: bridge method public final int size() { return this.getSize(); } //...and so on}
private Object deletedItem;
private final List innerList;
public final Object getDeletedItem() {
return this.deletedItem;
public final void setDeletedItem(@Nullable Object var1) {
this.deletedItem = var1;
public boolean remove(Object element) {
this.deletedItem = element;
return this.innerList.remove(element);
public final Object recover() {
public ListWithTrash() {
this((List)null, 1, (DefaultConstructorMarker)null);
public int getSize() {
return this.innerList.size();
// $FF: bridge method
public final int size() {
return this.getSize();
//...and so on
注: 生成されたコードで、Kotlin コンパイラは Decorator パターンという別のデザイン パターンを使ってクラス委譲をサポートしています。Decorator パターンでは、デコレータ クラスがデコレートされるクラスと同じインターフェースを共有します。デコレータ クラスは、ターゲット クラスの内部参照を保持し、そのインターフェースで提供されるすべてのパブリック メソッドをラップ(デコレート)します。
委譲は、特定のクラスを継承できない場合に特に便利です。クラス委譲を使うと、クラスが他のクラスの階層に含まれることはなくなります。その代わり、同じインターフェースを共有し、元の型の内部オブジェクトをデコレートします。つまり、パブリック API を維持したまま、実装を簡単に入れ替えることができます。
by キーワードを使うと、クラス委譲だけでなく、プロパティを委譲することもできます。プロパティ委譲では、デリゲートはプロパティの get 関数と set 関数の呼び出しを担当します。他のオブジェクトで getter/setter ロジックを再利用しなければならない場合、対応するフィールドだけでなく機能を簡単に拡張することができるので、この機能が非常に便利です。
get
set
次のような定義の Person クラスがあったとしましょう。
Person
class Person(var name:String, var lastname:String)
このクラスの name プロパティには、いくつかのフォーマット要件があります。name を設定するとき、先頭の文字が大文字、他の文字が小文字になるようにします。さらに、 name を更新する場合、updateCount プロパティを自動的にインクリメントします。
name
updateCount
この機能は、次のように実装してもいいかもしれません 。
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->class Person(name: String, var lastname: String) { var name: String = name set(value) { field = value.toLowerCase().capitalize() updateCount++ } var updateCount = 0}
class Person(name: String, var lastname: String) {
var name: String = name
set(value) {
field = value.toLowerCase().capitalize()
updateCount++
var updateCount = 0
これは動作しますが、要件が変わって lastname が変更されたときも updateCount をインクリメントすることになるとどうでしょうか。ロジックをコピーして貼り付け、カスタムの setter を書いてもいいかもしれませんが、両方のプロパティにまったく同じ setter を書いていることに気づくでしょう。
lastname
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->class Person(name: String, lastname: String) { var name: String = name set(value) { field = value.toLowerCase().capitalize() updateCount++ } var lastname: String = lastname set(value) { field = value.toLowerCase().capitalize() updateCount++ } var updateCount = 0}
class Person(name: String, lastname: String) {
var lastname: String = lastname
どちらの setter メソッドもほぼ同じということは、どちらかは不要ということです。プロパティ委譲を使うと、getter と setter をプロパティに委譲してコードを再利用できます。
クラス委譲と同じように、by を使ってプロパティを委譲します。すると、プロパティ構文を使ったときに、Kotlin はデリゲートを使うコードを生成します。
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->class Person(name: String, lastname: String) { var name: String by FormatDelegate() var lastname: String by FormatDelegate() var updateCount = 0}
var name: String by FormatDelegate()
var lastname: String by FormatDelegate()
この変更を行うと、name プロパティと lastname プロパティが FormatDelegate クラスに委譲されます。FormatDelegate のコードを確認してみましょう。デリゲート クラスは、getter だけを委譲する場合は ReadProperty<Any?, String> を、getter と setter の両方を委譲する場合は ReadWriteProperty<Any?, String> を実装する必要があります。この例の FormatDelegate は、setter が呼び出された場合にフォーマット処理を行うので、ReadWriteProperty<Any?, String> を実装しなければなりません。
FormatDelegate
ReadProperty<Any?, String>
ReadWriteProperty<Any?, String>
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->class FormatDelegate : ReadWriteProperty<Any?, String> { private var formattedString: String = "" override fun getValue( thisRef: Any?, property: KProperty<*> ): String { return formattedString } override fun setValue( thisRef: Any?, property: KProperty<*>, value: String ) { formattedString = value.toLowerCase().capitalize() }}
class FormatDelegate : ReadWriteProperty<Any?, String> {
private var formattedString: String = ""
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
return formattedString
override fun setValue(
property: KProperty<*>,
value: String
formattedString = value.toLowerCase().capitalize()
getter 関数と setter 関数に 2 つの追加パラメータがあることに気づいた方もいらっしゃるでしょう。最初のパラメータ thisRef は、プロパティを含むオブジェクトを表します。これを使うと、オブジェクト自体にアクセスし、他のプロパティを確認したり、他のクラス関数を呼び出したりできます。2 つ目のパラメータは KProperty<*> です。これは、委譲されたプロパティについてのメタデータにアクセスするために使うことができます。
thisRef
KProperty<*>
先ほどの要件を思い出してみてください。thisRef を使って updateCount プロパティにアクセスし、インクリメントしてみましょう。
<!-- Copyright 2019 Google LLC.SPDX-License-Identifier: Apache-2.0 -->override fun setValue( thisRef: Any?, property: KProperty<*>, value: String) { if (thisRef is Person) { thisRef.updateCount++ } formattedString = value.toLowerCase().capitalize()}
if (thisRef is Person) {
thisRef.updateCount++
この仕組みを理解するため、逆コンパイルした Java コードを見てみます。Kotlin コンパイラは、name プロパティと lastname プロパティについての FormatDelegate オブジェクトへのプライベートな参照を保持するためのコードと、追加したロジックを含む getter/setter の両方を生成します。
さらに、委譲されるプロパティを保持する KProperty[] も作成しています。name プロパティに対して生成された getter と setter を見てみると、インスタンスはインデックス 0 に保存されています。一方、lastname プロパティはインデックス 1 に保存されています。
KProperty[]
public final class Person { // $FF: synthetic field static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))}; @NotNull private final FormatDelegate name$delegate; @NotNull private final FormatDelegate lastname$delegate; private int updateCount; @NotNull public final String getName() { return this.name$delegate.getValue(this, $$delegatedProperties[0]); } public final void setName(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.name$delegate.setValue(this, $$delegatedProperties[0], var1); } //...}
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))};
@NotNull
private final FormatDelegate name$delegate;
private final FormatDelegate lastname$delegate;
private int updateCount;
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
//...
この仕組みによって、通常のプロパティ構文を使って任意の呼び出し元が委譲されるプロパティにアクセスできるようになっています。
person.lastname = “Smith” //
println(“Update count is $person.count”)
Kotlin は単に委譲をサポートしているだけではありません。Kotlin 標準ライブラリで組み込みの委譲も提供していますが、詳しくは別の記事で説明したいと思います。
委譲は他のオブジェクトにタスクを委譲する際に役立ち、コードの再利用性を高めます。Kotlin コンパイラは、委譲をシームレスに使えるようにコードを作成します。Kotlin は、by キーワードを使ったシンプルな構文でプロパティやクラスの委譲を行います。Kotlin コンパイラは、パブリック API を一切変更せず、委譲をサポートするために必要なすべてのコードを内部的に生成します。簡単に言えば、Kotlin は委譲に必要なボイラープレート コードをすべて生成して維持してくれます。つまり、委譲を Kotlin に委譲することができるのです。
Reviewed by Yuichi Araki - Developer Relations Team