[go: up one dir, main page]

この記事は Florina Muntenescu による Android Developers - Medium の記事 "Fewer crashes and more stability with Kotlin" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。


ユーザーは皆さんのアプリにシームレスな体験を期待しています。アプリがクラッシュすれば、低評価のレビューやアンインストールが増え、ブランドにダメージを与えてしまうでしょう。その一方で、コミュニティの皆さんとの対話の中で、Kotlin を採用する主な理由の 1 つはコードの高い安全性であるということをよく耳にします。実際に何社かのパートナーは、Kotlin を使ってコードの安定性を改善しています。この投稿では、その方法をいくつか紹介するとともに、Google Play ストアの統計結果にも注目し、Kotlin とクラッシュ数との間に相関関係があるかどうかを確認してみたいと思います。

アプリの品質

アプリの品質が影響するのは、ユーザー エクスペリエンスだけではありません。クラッシュ多いと、他のいくつかの要素にも悪影響が生じます。


  • アプリの見つけやすさ — Google Play ストアのおすすめは、人間による選定とアルゴリズムによる計算の組み合わせで行われています。この計算においては、アプリの品質が特に重要な考慮事項となっています。

  • ブランド — プロダクトのパフォーマンスは評価やレビューに影響する可能性があり、それがブランドに影響する可能性があります。

  • (エンゲージメントの高い)ユーザーの多さ — 実質的なトラフィックとブランドの認知が上昇すれば、ユーザーの獲得と維持が促進される可能性が上がります。それにより、エンゲージメントや下位ファネルの指標にも影響が出る可能性があります。


Kotlin で構築したアプリはクラッシュする可能性が 20% 低い


Google Play のトップ 1,000 アプリを調査してみたところ、Kotlin を使っているアプリは、それ以外のアプリよりもユーザー 1 人あたりのクラッシュ発生率が 20% 低いことがわかりました。

その具体例として、コードの 74% が Kotlin である Swiggy のエンジニアリング チームは、新機能の開発を Kotlin に移行して以来、クラッシュを 50% 減らしました。こういった結果を Kotlin はどのように実現しているのか見ていきましょう。


NullPointerException を回避する

Google Play でのクラッシュの原因ナンバーワンは NullPointerException です。Google Home チームは、2018 年よりすべての新機能を Kotlin で書いています。その結果、1 年前と比べて null ポインタによるクラッシュが 33% 減少しました。


Google Home が NullPointerException を 33% 削減


NullPointerException を避けるには、メソッドを呼び出したりメンバーにアクセスしたりする前に、扱っているオブジェクトの参照が null でないことを確認しなければなりません。Kotlin では、null 可能性が型システムの一部として組み込まれています。たとえば、変数は最初から null 可能か null 不可能かを宣言する必要があります。null 可能性を型システムの一部として組み込むことで、コードベースについての自分の記憶や知識、またコンパイル時警告(フィールドやパラメータに @Nullable アノテーションを付けた場合)に頼る必要はなくなります。null 可能性を強制することで、単なる警告ではなく、コンパイル時にエラーが発生します。null 可能性を扱う方法については、こちらのページをご覧ください。


一般的な問題を回避する

私たちデベロッパーは、気づかないうちにアプリにたくさんの問題を紛れ込ませています。そのほとんどはあまりに軽微で、調査するのは難しいかもしれません。そのような問題のうち、Kotlin を使うことで回避できるものをいくつか紹介しましょう。


hashCode() と equals()

2 つのオブジェクトが等しい場合、それらのハッシュコードも同じである必要があります。しかし、どちらかのメソッドを実装し忘れたり、クラスに新しいプロパティを追加したときに更新し忘れてしまったりすることがよくあります。データを保持するためだけのクラスを扱う場合は、Kotlin のデータクラスを使うようにしましょう。データクラスを使うと、コンパイラが hashCode()equals() を生成してくれるので、クラスのプロパティを変更すると自動的に更新されます。


構造等価性と参照等価性

2 つのオブジェクトは構造が等しい(中身が同じ)のでしょうか。それとも、参照が等しい(ポインタが同じ)のでしょうか。Java プログラミング言語では、プリミティブには常に == を使います。そのため、実際には構造が等しいかどうかを確認(equals() を呼び出してチェック)したいのに、オブジェクトにも ==(参照が等しい)を使ってしまうというのがよくある誤りです。まず、Kotlin にはプリミティブ型はなくIntString などのクラスを使います。つまり、すべてがオブジェクトなので、オブジェクトとプリミティブ型の区別を気にする必要はありません。次に、Kotlin では == は構造が等しい、=== は参照が等しいと定義されています。したがって、誤って参照が等しいかどうかを確認してしまうことはありません。


else if else if else では十分でない場合にどうするか

enum を扱う場合、通常はすべての可能なケースを記述しなければなりません。そのため、switchif else チェーンを使うことになります。enum を修正して新しい値を追加する場合、enum を使っている各コード スニペットを手動でチェックし、新しいケースに対応しているかどうかを確認する必要があります。しかし、これはエラーにつながりがちです。Kotlin では、when を式として使うと、この確認をコンパイラに任せることができます。すべての可能な分岐に対応していない場合、コンパイラ エラーが発生します。


まとめ

ユーザーやブランドにとって、アプリの安定性は重要です。Kotlin を使い始めてクラッシュ率を減らし、ユーザーの満足度を高めてアプリの高評価を守り、ユーザーの維持や獲得につなげましょう。

詳しくは、Kotlin でより優れたアプリを作成するためにできることをお読みください。また、ケーススタディをご覧いただき、Kotlin のメリットをご確認ください。世界でデベロッパーに広く使われている言語の 1 つである Kotlin を使ってみたい方は、入門ページをご覧ください。


Reviewed by Yuichi Araki - Developer Relations Team and Nori Fujii - Google Play Developer Marketing, APAC

この記事は Florina Muntenescu による Android Developers - Medium の記事 "Don’t argue with default arguments" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。


Kotlin Vocabulary: デフォルト引数


短くて使いやすいデフォルト引数を利用すると、ボイラープレートを書くことなく関数のオーバーロードを実現できます。多くの 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 つのパラメータ ToyintObject を受け取ります。デフォルト引数が使われたときに呼び出されます。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 関数を拡張し、この関数を呼び出す際に doggotoy をデフォルト引数として使うようにします。


/* 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 関数ではこの値が使われることはありません。このパラメータは、オーバーライドする関数でデフォルト値をサポートするために使用されています。


デフォルト引数と継承


デフォルト引数がある関数をオーバーライドすると、何が起きるでしょうか。

先ほどの例を次のように変更してみましょう。


  • playDoggoOpen 関数にし、DoggoOpen クラスにする。
  • 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 ではなく、DefaultConstructorMarkernull で呼び出されています。


/* 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 の投稿を確認してみてください。



この記事は Murat Yener による Android Developers - Medium の記事 "Delegating Delegates to Kotlin" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。


Kotlin Vocabulary: 委譲


仕事を完了する方法の 1 つは、その仕事を他者に委譲することです。皆さんの仕事を友だちに委譲することを言っているわけではありません。今回のテーマは、あるオブジェクトから別のオブジェクトに委譲することです。


ソフトウェアの世界では、委譲という考え方は新しいものではありません。委譲はデザイン パターンの 1 つで、あるオブジェクトが デリゲートと呼ばれるヘルパー オブジェクトに委譲することでリクエストを処理することを指します。デリゲートの役割は、元のオブジェクトに代わってリクエストを処理し、その結果を元のオブジェクトが利用できるようにすることです。


Kotlin はクラス委譲とプロパティ委譲をサポートしているので、委譲を簡単に扱えます。さらに、いくつかの独自の組み込みデリゲートも提供しています。

クラス委譲

最後に削除された項目を復元できる ArrayList を使うとしましょう。基本的に、必要なのは同じ ArrayList の機能だけですが、最後に削除された項目への参照が必要です。


これを実現する方法の 1 つは、ArrayList クラスを拡張することです。この新しいクラスは、MutableList インターフェースの実装ではなく ArrayList の具象クラスを拡張したものなので、ArrayList の具象クラスの実装と強く結合されることになります。


MutableList の実装で remove() 関数をオーバーライドし、削除した項目の参照を保持できるようにしたうえで、その他の空の実装を他のオブジェクトに委譲したいと思ったことはありませんか?Kotlin では、これを実現する方法が提供されています。具体的には、内部 ArrayList インスタンスに作業の大半を委譲し、その動作をカスタマイズできます。これを行うため、Kotlin には新しいキーワード by が導入されています。


では、クラス委譲の仕組みを確認してみましょう。by キーワードを使うと、Kotlin は 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

   }

}


by キーワードは、MutableList インターフェースの機能を innerList という名前の内部 ArrayList インスタンスに委譲するよう Kotlin に伝えます。内部 ArrayList オブジェクトに直接橋渡しするメソッドが提供されるので、ListWithTrashMutableList インターフェースのすべての機能をサポートします。さらに、独自の動作を追加することもできるようになります。

内部処理

動作の仕組みを確認してみましょう。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

}


注: 生成されたコードで、Kotlin コンパイラは Decorator パターンという別のデザイン パターンを使ってクラス委譲をサポートしています。Decorator パターンでは、デコレータ クラスがデコレートされるクラスと同じインターフェースを共有します。デコレータ クラスは、ターゲット クラスの内部参照を保持し、そのインターフェースで提供されるすべてのパブリック メソッドをラップ(デコレート)します。


委譲は、特定のクラスを継承できない場合に特に便利です。クラス委譲を使うと、クラスが他のクラスの階層に含まれることはなくなります。その代わり、同じインターフェースを共有し、元の型の内部オブジェクトをデコレートします。つまり、パブリック API を維持したまま、実装を簡単に入れ替えることができます。

プロパティ委譲

by キーワードを使うと、クラス委譲だけでなく、プロパティを委譲することもできます。プロパティ委譲では、デリゲートはプロパティの get 関数と set 関数の呼び出しを担当します。他のオブジェクトで getter/setter ロジックを再利用しなければならない場合、対応するフィールドだけでなく機能を簡単に拡張することができるので、この機能が非常に便利です。


次のような定義の Person クラスがあったとしましょう。


class Person(var name:String, var lastname:String)


このクラスの name プロパティには、いくつかのフォーマット要件があります。name を設定するとき、先頭の文字が大文字、他の文字が小文字になるようにします。さらに、 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

}


これは動作しますが、要件が変わって lastname が変更されたときも updateCount をインクリメントすることになるとどうでしょうか。ロジックをコピーして貼り付け、カスタムの setter を書いてもいいかもしれませんが、両方のプロパティにまったく同じ setter を書いていることに気づくでしょう。


<!-- 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

}


どちらの 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

}


この変更を行うと、name プロパティと lastname プロパティが FormatDelegate クラスに委譲されます。FormatDelegate のコードを確認してみましょう。デリゲート クラスは、getter だけを委譲する場合は ReadProperty<Any?, String> を、getter と setter の両方を委譲する場合は ReadWriteProperty<Any?, String> を実装する必要があります。この例の FormatDelegate は、setter が呼び出された場合にフォーマット処理を行うので、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()

   }

}


getter 関数と setter 関数に 2 つの追加パラメータがあることに気づいた方もいらっしゃるでしょう。最初のパラメータ thisRef は、プロパティを含むオブジェクトを表します。これを使うと、オブジェクト自体にアクセスし、他のプロパティを確認したり、他のクラス関数を呼び出したりできます。2 つ目のパラメータは 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()

}


内部処理

この仕組みを理解するため、逆コンパイルした Java コードを見てみます。Kotlin コンパイラは、name プロパティと lastname プロパティについての FormatDelegate オブジェクトへのプライベートな参照を保持するためのコードと、追加したロジックを含む getter/setter の両方を生成します。

さらに、委譲されるプロパティを保持する KProperty[] も作成しています。name プロパティに対して生成された getter と setter を見てみると、インスタンスはインデックス 0 に保存されています。一方、lastname プロパティはインデックス 1 に保存されています。


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);

  }

  //...

}


この仕組みによって、通常のプロパティ構文を使って任意の呼び出し元が委譲されるプロパティにアクセスできるようになっています。


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