Deep dive into delegated properties: разбираемся с делегатами в котлин
Не так давно решил изучить официальную документацию котлина. В свое время изучал его, как и многие другие, через видосики и практику. Поэтому решил поглубже погрузиться в дебри.
Остановил свое внимание на делегатах, так как нашел несколько особенностей применения, которые почти ни где не упоминаются, а что то и вовсе забыли описать в оф. документации, уверен что многим будет полезно.
Введение
Кратко про делегированные свойства — это просто функции для делегирования обращения (чтения и запись) к свойству с помощью ключевого слова by. Для реализации нужно определить операторы getValue или setValue для класса. Для помощи и простоты создания этих операторов, можно воспользоваться готовыми интерфейсами по типу ReadWriteProperty, ReadOnlyProperty или уже готовыми функциями из kotlin stdlib: notNull, lazy, observable, vetoable. Дальше капнем уже глубже, а базу советую посмотреть в официальной документации.
Хранение в мапе и делегирование другому проперти
1. Геттер и сеттер одного проперти можно делегировать другому проперти
Делегируемое свойство может быть
1 — top level property
2 — свойство класса или расширение
Для делегирования проперти другому проперти используется синтаксис ::MyClass::delegate
или this::delegate
class MyClass(var myClassProperty: Boolean = false)
val clazz = MyClass()
var delegated by clazz::myClassProperty
var notDelegated = clazz.myClassProperty
В данном примере clazz::myClassProperty
тоже самое что clazz.myClassProperty
, разница лишь в том, что для notDelegated проперти создается дополнительный инстанс, а для delegated нет. Получается замена ручному определению get () и set (value).
Полное делегирование обращения к проперти, без дополнительной логики, редко когда может пригодится, в оф. доке приводят пример переименования и сохранения обратной совместимости.
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// Notification: 'oldName: Int' is deprecated.
// Use 'newName' instead
myClass.oldName = 42
println(myClass.newName) // 42
}
В пример из официальной документации, старый проперти со старым именем помечаем аннотацией Deprecated и все обращения теперь будут переадресовываться на проперти с новым названием.
2. Использование инстанс map как делегат для хранения пропрети
class Properties(map: Map) {
val name by map
val version by map
}
val properties = Properties(
mapOf(
"name" to "delegate testing",
"version" to "0.0.1"
)
)
fun main() {
println(properties.name) // delegate testing
println(properties.version) // 0.0.1
}
Мапы часто могут возвращаться при простом париснге ответа json или какой-нибудь конфинг с параметрами, и использование делегированного проперти в данном случае сделают код более читабельным. В примере сверху, мы берем значения из мапы через строковые ключи, которыми являются имена проперти.
Множественные ресиверы
Для одного делегата мы можем иметь множество getValue и setValue с разными аргументами thisRef: ContextType
, разные определения методов будут вызваны в разных ситуациях. Это может быть очень полезно, в примере снизу для Fragment и Activity getValue отработает по разному, в зависимости от контекста.
class CachedPropertyDelegate {
operator fun getValue(
activity: Activity,
prop: KProperty<*>
): String {
return "delegated property from Activity"
}
operator fun getValue(
fragment: Fragment,
prop: KProperty<*>
): String {
return "delegated property from Fragment"
}
}
Расширения
Самым неочевидным способ объявления делегата является создание свойства расширения, благородя чему мы можем использовать преимущество и лаконичность делегатов, не засоряя наш класс дополнительными методами или вообще классами.
class UserInfo(
val name: String,
val lastName: String
)
operator fun UserInfo.getValue(thisRef: Nothing?, property: KProperty<*>): String {
val fullName = "$name $lastName"
println("access to $fullName")
return fullName
}
fun main() {
val user = UserInfo("John", "Doe")
val fullName by user
println(fullName)
}
// output
// access to John Doe
// John Doe
Провайдеры (Providing a delegate)
Для делегата можно переопределить не только операторы getValue и setValue, а так же и provideDelegate. Это функция возвращает инстант нашего делегата при определении его ключевым словом by
. При переопределении данного делегата можно выполнить дополнительный код, что может быть очень даже полезно. Так же есть вспомогательный интерфейс PropertyDelegateProvider
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class FileFormat(val fileFormat: String)
private val directoryPath get() = System.getProperty("user.dir") + "\\src\\main\\kotlin\\testdir"
class TextWriterDelegate(private val text: String) {
private var file: File? = null
operator fun getValue(thisRef: Nothing?, property: KProperty<*>): String {
return file?.readText() ?: error("No such file")
}
operator fun provideDelegate(thisRef: Nothing?, property: KProperty<*>): TextWriterDelegate {
val format = property.findAnnotation()?.fileFormat?.let { ".$it" } ?: ".txt"
val dir = File(directoryPath)
dir.mkdir()
file = File(dir, property.name + format)
file?.writeText(text)
return this
}
}
fun texting(action: StringBuilder.() -> Unit) = TextWriterDelegate(buildString(action))
/** */
@FileFormat("txt")
val appConfig by texting {
val properties = listOf(
"name" to "delegate testing",
"version" to "0.0.1"
)
properties.forEach {
appendLine("${it.first} = ${it.second}")
}
}
В примере сверху создаётся файл с именем проперти appConfig.txt
и записываются строчки текста, которые мы прописали при объявлении делегата.
Заключение
Успехов вам в изучении и прокачке навыков!
Если вам статья была интересной, то можете переходить в мой телеграм канал, куда я буду постить свои дальнейшие находки и мысли.