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<String, String>) { 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>()?.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
и записываются строчки текста, которые мы прописали при объявлении делегата.