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
オブジェクトに直接橋渡しするメソッドが提供されるので、ListWithTrash
は MutableList
インターフェースのすべての機能をサポートします。さらに、独自の動作を追加することもできるようになります。
内部処理
動作の仕組みを確認してみましょう。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