事件は起きる
いつものように、アプリ開発に精を出していたら、いやーなエラーが出ちゃいました。
Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
元凶となるコード⇩
var mapView:MapView? by remember { mutableStateOf(null)}
このエラーは「null を許容しない型の状態を管理しようとすると、コンパイラが getValue メソッドを見つけられず、デリゲートとして使用できないと判断する」ために発生する。とGeminiさんが教えてくれましたが、意味がわかりません。
この意味を理解するために、Delegateの基礎から向き合っていこう。そんな記事になります
Delegate(委譲)ってそもそも何
Delegateとは何を委譲しているんでしょう??
よくある説明として
「クラスやプロパティの処理を別のオブジェクトに委譲する機能です。これにより、コードの再利用性や可読性を高めることができます」
ってのがありますが、意味わかりませんね。
具体例を見ながら理解していきたいと思います
値を取得、設定して、コンソール出力する処理をDelegateあり/なしで考えてみましょう
- Delegateなし
MyClass()の中でget()・set()を定義して、myPropertyの値の取得と設定を実現しています
class MyClass {
private var _myProperty = "初期値"
var myProperty: String
get() {
println("myProperty の値を取得: $_myProperty")
return _myProperty
}
set(value) {
println("myProperty の値を変更: $_myProperty -> $value")
_myProperty = value
}
}
fun main() {
val myObject = MyClass()
println(myObject.myProperty)
myObject.myProperty = "新しい値"
println(myObject.myProperty)
}
- Delegateあり
MyClassはmyPropertyの宣言のみを行なっており、get(),set()の役割はLoggedPropertyに委譲しています
import kotlin.reflect.KProperty
class LoggedProperty<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("${property.name} の値を取得: $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("${property.name} の値を変更: $value -> $newValue")
value = newValue
}
}
class MyClass {
var myProperty: String by LoggedProperty("初期値")
}
fun main() {
val myObject = MyClass()
println(myObject.myProperty) // ログ出力と値の取得
myObject.myProperty = "新しい値" // ログ出力と値の変更
println(myObject.myProperty) // ログ出力と値の取得
}
このようにLoggedPropertyに委譲することで、例えばMyClass以外にも値を取得、設定して、コンソール出力する処理が必要な場合にコードの冗長性を回避することができます。
- 冗長性が回避できる
import kotlin.reflect.KProperty
class LoggedProperty<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("${property.name} の値を取得: $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("${property.name} の値を変更: $value -> $newValue")
value = newValue
}
}
class MyClass {
var myProperty: String by LoggedProperty("初期値")
}
class MyClass2 {
var myProperty: String by LoggedProperty("こんにちは")
}
fun main() {
val myObject = MyClass()
println(myObject.myProperty) // ログ出力と値の取得
myObject.myProperty = "新しい値" // ログ出力と値の変更
println(myObject.myProperty) // ログ出力と値の取得
}
- Delegateを使わないと。。
class MyClass {
private var _myProperty = "初期値"
var myProperty: String
get() {
println("myProperty の値を取得: $_myProperty")
return _myProperty
}
set(value) {
println("myProperty の値を変更: $_myProperty -> $value")
_myProperty = value
}
}
class MyClass2 {
private var _myProperty = "こんにちは"
var myProperty: String
get() {
println("myProperty の値を取得: $_myProperty")
return _myProperty
}
set(value) {
println("myProperty の値を変更: $_myProperty -> $value")
_myProperty = value
}
}
fun main() {
val myObject = MyClass()
println(myObject.myProperty)
myObject.myProperty = "新しい値"
println(myObject.myProperty)
val myObject = MyClass2()
println(myObject.myProperty)
myObject.myProperty = "新しい値"
println(myObject.myProperty)
}
rememberにおけるDelegateについて知る
何となくDelegateの意味はわかりました。
じゃあ、冒頭のエラーの原因となっているコード⇩には、どのようにDelegateが関わっているのかを調べます
var mapView:MapView? by remember { mutableStateOf(null)}
先ほど見た通り、これはmapViewという:MapView型の変数のget(),set()処理を mutableStateOf(null)のオブジェクトに委譲している。ということを意味しているはずです。
mutableStateOf()はのget(),set()処理はどのようになっているのでしょう??
一応確認してみましょう。⇩こんな感じらしいです。
オブジェクトの参照であるthisRefを受け取って、T型の値を返すかセットするっていうシンプルなものですね
operator fun <T> MutableState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
operator fun <T> MutableState<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
で、ですよ。なぜ以下のエラーが発生するかを調べていくと、、
Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
var mapView:MapView? by remember { mutableStateOf(null)}
MapView の型が MutableState のデリゲート (by) に適用できない型の可能性がある。
という結論で納得しました。。。
単純に以下のimport文が足りていないことが原因でした
(コメントいただいた@utsumi2024さんありがとうございました)
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
MapView の型が MutableState のデリゲート (by) に適用できないことが原因だと思う!! と思い込んでいたところ、誤りを指摘いただいて勉強になりました。
<T>はジェネリクスなので、MapViewの型にも対応しているはず。使えないのはimportが単純に足りていないからでは?って感じですね。
operator fun <T> MutableState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
おわり
という感じで、import文が不足している。みたいな結論でしたが、
結果Delegateを学ぶいいきっかけになりました〜〜
おしまいです〜〜ちょっとは理解できた気がしました!