Deep dive into delegated properties: разбираемся с делегатами в котлин

eb7bf553b33a10fecf6aeb22e174181e

Не так давно решил изучить официальную документацию котлина. В свое время изучал его, как и многие другие, через видосики и практику. Поэтому решил поглубже погрузиться в дебри.

Остановил свое внимание на делегатах, так как нашел несколько особенностей применения, которые почти ни где не упоминаются, а что то и вовсе забыли описать в оф. документации, уверен что многим будет полезно.

Введение

Кратко про делегированные свойства — это просто функции для делегирования обращения (чтения и запись) к свойству с помощью ключевого слова 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 и записываются строчки текста, которые мы прописали при объявлении делегата.

Заключение

Успехов вам в изучении и прокачке навыков!

Если вам статья была интересной, то можете переходить в мой телеграм канал, куда я буду постить свои дальнейшие находки и мысли.

© Habrahabr.ru