Kotlin — ещё меньше копипасты с делегатами локальных переменных

TLDR: используем имя переменной при инициализации ее значения

Допустим, в тесте нужно создать некоторое количество однотипных объектов, у которых есть характерное отличие в одном строковом свойстве. Для примера возьмем класс Color.

data class Color(val value: String)

Создадим несколько экземпляров, дадим переменным осмысленные имена.

val red = Color("red")
val green = Color("green")
val blue = Color("blue")

Имена переменных дублируют параметр конструктора. Копипаста. Давайте ее изживём.

val red by color()
val green by color()
val blue by color()

println(red.value)   // "red"
println(green.value) // "green"
println(blue.value)  // "blue"

Разберемся, что тут происходит.

Ключевое слово by подсказывает - функция color() возвращает делегат - объект со специальным методом, которому адресуется чтение свойства или локальной переменной. Второй параметр метода - это описание свойства, откуда можно узнать имя переменной.

Устройство делегата тривиально - при первом обращении к переменной создаем наш объект, запоминаем его и возвращаем, при последующих вызовах возвращаем его же.

class ColorDelegate {
    
  private var color: Color? = null
  
  operator fun getValue(thisRef: Nothing?, property: kotlin.reflect.KProperty<*>): Color {
    val res: Color = color ?: Color(property.name)
    color = res
    return res
  }
}

fun color(): ColorDelegate {
  return ColorDelegate()
}

Функция color() решает только эстетическую задачу, вместо ее вызова можно явно создавать ColorDelegate.

Спасибо! )

UPD: Как и любую магию, описанный прием нужно применять с осторожностью. Если имя несет еще функции, кроме маркерной - не стоит. И почти наверняка не стоит его применять в продуктовом коде.

@moonster
07.07.2022 14:45 UTC
Первоисточник

Комментарии

@Pab10
07.07.2022 11:42 UTC
+5

Давайте вместо простого и понятного вызова конструктора родим дополнительную абстракцию с генериками и поржем над нубасами, которые пытаются въехать в проект. То, что язык дает такие возможности это прекрасно, но не забывайте про бритву. Подберите более релевантный пример, пожалуйста :)

@moonster
07.07.2022 12:22 UTC
+2

Мой реальный кейс, где я впервые применил механизм, вам бы понравился.

Был у меня тест с таким сценарием:

1 - создаем N ссылок на файлы

2 - раскладываем ссылки в специальную структуру

3 - применяем команду к структуре

4 - проверяем, как структура изменилась

Свойства ссылки в рамках структуры зависит места, которое она занимает в структуре, но при этом сама ссылка никакой информации о своем месте не содержит (и не должна).

Ссылок много. Ну и в целом тест оказался не очень простой - код занял полтора экрана.

Для упрощения читаемости теста имена переменных ссылок на файлы я сделал говорящими. Но этого оказалось мало, т.к. при дебаге в дебрях тестируемого кода имена исходных переменных уже недоступны. Поэтому я продублировал имя переменной в поле ссылки вручную, а потом и придумал, как это делать автоматически.

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

@Beholder
07.07.2022 13:05 UTC
+2

Но только упаси вас бог переименовать такую переменную при "незначительном" рефакторинге.

Было:

val red by color()

Захотели ясности:

val foregroundColor by color() // было "red"

@moonster
07.07.2022 13:14 UTC
0

Справедливое замечание, добавил.

@Lewigh
07.07.2022 16:50 UTC
0

Думаю наиболее полезно было бы при работе с БД, где из имени свойства можно было бы по умолчанию выводить имя колонки

@moonster
07.07.2022 17:03 UTC
0

Не соглашусь.

Допустим, мы зачем-то решили сами написать компонент, который генерит SQL или мапит ResultSet.

Тогда проще взять имена полей более традиционным способом, примерно так:

    data class SomeEntity(val prop1: String, val prop2: Int)

    val properties = SomeEntity::class.declaredMemberProperties

    val someEntity = SomeEntity(prop1 = "Hello", prop2 = 100500)

    properties.forEach { prop ->
        println(prop.name + " = " + prop.get(someEntity))
    }
    // ==>
    //prop1 = Hello
    //prop2 = 100500

@Razoomnick
07.07.2022 23:03 UTC
+2

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

#define TRUE FALSE // счастливой отладки суки

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