Kotlin は C# の INotifyPropertyChanged の夢を見るか?

エイプリルフールなので、ポエムを書きます。

Kotlin が仮に .NET で動いたらのお話し。全体のソースコードはこちら

背景

WPF でアプリを書く際、ライブラリを使わない場合は ViewModel のクラスには毎回のように INotifyPropertyChanged インターフェースの実装を書いていたので、面倒でした。

ところ変わって、 Kotlin の Delegated Properties という構文は衝撃的で、面白い何かに応用できそうだと思っています。

他に継承させたいクラスがある場合を考えると、 INotifyPropertyChanged は必ずインターフェースとして実装したいですし、それは C# の他のライブラリでもできるので意味がないです。

結果

解説の前にまずは結果を。

Main.kt
4
5
6
7
8
9
10
11
12
class MainViewModel : NotifyPropertyChanged {
fun Test() {
// do your complex job

raisePropertyChanged("AnotherProperty")
}

var fullName: String by PropertyChangedDelegate("Hoge Taro")
}
Main.kt
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
fun main(args: Array<String>) {
val mainVm = MainViewModel()
val subVm = object : NotifyPropertyChanged {
var step: Int by PropertyChangedDelegate(9876)
}

mainVm.propertyChanged += { sender, e ->
println("Notified ${e.propertyName} [$sender]")
}
mainVm.propertyChanged += { sender, e ->
println("This is additional for ${e.propertyName}")
}

subVm.propertyChanged += { sender, e ->
println("Notified ${e.propertyName} [$sender]")
}

mainVm.Test()

println(mainVm.fullName)
mainVm.fullName = "Piyo Jiro"
println(mainVm.fullName)

println(subVm.step)
subVm.step = 12345
println(subVm.step)
}

実行結果は以下の通りです。

1
2
3
4
5
6
7
8
9
Notified AnotherProperty [MainViewModel@246b179d]
This is additional for AnotherProperty
Hoge Taro
Notified fullName [MainViewModel@246b179d]
This is additional for fullName
Piyo Jiro
9876
Notified step [MainKt$main$subVm$1@25f38edc]
12345

解説

+= によるイベント追加のからくり

Kotlin では特定の名前の関数を operator で修飾すると、演算子の記号としてその関数を呼び出せるようになる。
このコードの EventHandler クラスのように plusAssign() 関数を記述することで、 += が使えるようになる。なお、下の invoke() 関数も同じで、メソッド呼び出しのように呼び出せる。

fullName プロパティの後ろの by とは

これが Kotlin の Delegated Properties というもので、 getter と setter の処理を他のオブジェクトに任せることができます。
このコードの PropertyChangedDelegate クラスでは、 setValue() 関数内で raisePropertyChanged() 関数によって、イベントを発行しています。つまり、 C# の getter と setter に書いていたボイラープレートを、この機能を使って楽に書いています。
ちなみに、カッコ内は初期値とし、値があることを強制させました。( Kotlin は null かそうでないかに厳しいので)

なぜインターフェースなのに NotifyPropertyChanged がインスタンスを持っている(ように見える)のか

実は、 Kotlin はインターフェースの中にも状態のない実装なら書けます。

  1. インターフェースのプロパティは Delegated Property にできないが、メソッドの実行ならできる。
  2. インターフェース内でも this が使えてしまうので、実際のインスタンスに固有の番号も取得できてしまう。
  3. マップなり辞書を作成して、すべての NotifyPropertyChanged インターフェースを実装するインスタンスの中の propertyChanged() 関数を通して情報を取得する。

これを組み合わせた NotifyPropertyChanged インターフェースを作ることで、少なくとも見た目は NotifyPropertyChanged インターフェースを継承するだけでいろいろ使えるようになりました。

感想

こういうのはパフォーマンスの評価も必要だと思うんですけど、ちょっと面倒なのでしてないです。

あとは、 VM のインスタンスが消えた後も NotifyPropertyChanged インターフェースでのマップ・辞書のインスタンスは残ってることになるので、そのあたりも要検討です。

Kotlin の Delegated Properties については他サイトにわかりやすい解説があるので、そちらをオススメします。

参照記事