Posted at

Kotlin復習 〜Delegation 1〜

自分の復習(自己満です)のために Kotlinイン・アクション や kotlinlang.org の例を参考にしてまとめてみた。

とくにtipsなどはありませんので悪しからず。


クラス委譲

拡張できるように設計されていないクラスに手を加えたい場合、同じインターフェースを実装ししていたら by を使うことでインターフェースの実装を別オブジェクトに 委譲(dekegate) することができる。

Collectionを例にした簡単な例が↓です。


CountingSet.kt

class CountingSet<T>(

private val innerSet: MutableCollection<T> = HashSet()
) : MutableCollection<T> by innerSet { // MutableCollectionの実装をinnerSetに委譲している

var added = 0

override fun add(element: T): Boolean {
added++
return innerSet.add(element)
}

override fun addAll(c: Collection<T>): Boolean {
added += c.size
return innerSet.addAll(c)
}
}


要素を追加した回数を数えるコレクションです。

add と addAll メソッドをオーバーライドしていて、それ以外は委譲元のクラスの実装のまま。


Sample.kt

fun main(args: Array<String>) {

val cSet = CountingSet<Int>()
cSet.addAll(listOf(1, 1, 2))
println("${cSet.added} objects were added, ${cSet.size} remain")
// → 3 objects were added, 2 remain
}


メンバがいる場合


LetterInterface.kt

interface LetterInterface {

val title: String
fun printMessage()
}


ChainLetter.kt

class ChainLetter(x: String) : LetterInterface {

override val title = "これは $x です。"
override fun printMessage() = println(title)

}



LoveLetter.kt

class LoveLetter(letter: LetterInterface): LetterInterface by letter{

override val title = "これは ラブレター です。"
}


Sample.kt

fun main(args: Array<String>) {

val letter = ChainLetter("不幸の手紙")
val loveLetter = LoveLetter(letter)
loveLetter.printMessage()
// → これは 不幸の手紙 です。

println(loveLetter.title)
// → これは ラブレター です。
}


委譲されたオブジェクトは自身のインターフェースの実装にしかアクセスできない。


委譲プロパティ

構文は下記の通り

val/var <property name> : <Type> by <expression>

by の右側の expression がプロパティの委譲先となる。

委譲先のクラス(Delegateクラス)は getValue() , setValue() メソッドを持っている必要がある。(setValue()はミュータブル時のみ)


遅延初期化と by lazy()

遅延初期化(lazy initialization)は最初に参照された時にオブジェクトを生成するので、初期化処理に時間がかかる場合や、常にオブジェクトが必要ではないという場合に有効に作用する。


Sample.kt

private val lazyValue: String by lazy {

println("computed!")
"Hello"
}

fun main(args: Array<String>) {
println(lazyValue)
// → computed!
// → Hello
println(lazyValue)
// → Hello
}


最初のget()で lazy に渡されたラムダが実行(スレッドセーフ)され、結果を保持する。2回目以降は保持された値を返す。


委譲プロパティの実装

あるオブジェクトのプロパティに変更があった時、リスナー通知する処理をみてみる。

Javaの標準的な通知処理の PropertyChangeSupport と PropertyChangeEvent を委譲プロパティを使わずに Kotlin でどう使えるか確認し、その後で委譲プロパティを利用した形にする。


PropertyChangeAware.kt

// PropertyChangeSupportを利用するためのヘルパークラス

open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}

fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}



Person.kt

// プロパティ変更通知を手動で実装

class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

var age: Int = age
set(newValue) {
val oldValue = field // ← 「field」識別子でプロパティのバッキングフィールドにアクセス
field = newValue
changeSupport.firePropertyChange( // ← プロパティの値の変更についてリスナに通知
"age", oldValue, newValue
)
}

var salary: Int = salary
set(newValue) {
val oldValue = field // ← 「field」識別子でプロパティのバッキングフィールドにアクセス
field = newValue
changeSupport.firePropertyChange( // ← プロパティの値の変更についてリスナに通知
"salary", oldValue, newValue
)
}
}


上記 Person クラスの age と salary の setter は共通化できるので、プロパティの値をセットし、通知呼び出しする処理をクラス化する。


Person.kt

class ObservableProperty(

val propName: String, var propValue: Int,
val changeSupport: PropertyChangeSupport
) {

fun getValue(): Int = propValue

fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}

class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(value) {
_age.setValue(value)
}

val _salary = ObservableProperty("salary", salary, changeSupport)
var salary: Int
get() = _salary.getValue()
set(value) {
_salary.setValue(value)
}
}


この ObservableProperty が委譲プロパティの肝。

Kotlin の委譲プロパティを使えば setter/getter のボイラープレートを省略できる。

そのために ObservableProperty のメソッドシグネチャを Kotlin の規約の形に変更する。


Person.kt

class ObservableProperty(

var propValue: Int, val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}

// プロパティ変更の通知に委譲プロパティを使用
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
val age: Int by ObservableProperty(age, changeSupport)

val salary: Int by ObservableProperty(salary, changeSupport)
}



委譲プロパティ化前の ObservableProperty と比較


  • getValue, setValue の関数に operator がついた


    • 規約に基づく関数全てに必要



  • getValue, setValue にそれぞれ引数を2つ追加。


    • 1つ目:get, set されるプロパティのインスタンス

    • 2つ目:プロパティ自身に相当する値で、Kproperty 型のオブジェクトとして表現



  • プロパティ名(age, salary)は KProperty からアクセスできるので、プライマリコンストラクタから name プロパティを削除


標準ライブラリにある ObservableProperty と似たクラスを利用してみる


Person.kt

class Person(

val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
private val observer = { prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}

※ PropertyChangeSupport と関連付けられていないのでプロパティの変更通知をするラムダを渡す必要がある。


まとめ


  • 委譲を使わなくてもコードは書けるけど、使わない手はない

  • 今更だけど遅延初期化ステキ。プロパティの変更通知をオブサーブできるの地味に便利

  • 定期的に Kotlinイン・アクション や kotlinlang.org を見直すのは大事

  • まだ手元に Kotlinイン・アクション がない人は買いましょう!!