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: Как и любую магию, описанный прием нужно применять с осторожностью. Если имя несет еще функции, кроме маркерной - не стоит. И почти наверняка не стоит его применять в продуктовом коде.
Комментарии (7)
Lewigh
07.07.2022 19:50Думаю наиболее полезно было бы при работе с БД, где из имени свойства можно было бы по умолчанию выводить имя колонки
moonster Автор
07.07.2022 20:03Не соглашусь.
Допустим, мы зачем-то решили сами написать компонент, который генерит 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
08.07.2022 02:03+2Ох, не дай бог в реальности такое встретить. Имя переменной влияет на поведение программы, это даже круче классики
#define TRUE FALSE // счастливой отладки суки
Но для ненормального программирования была бы хорошая статья, возможность языка интересная.
Pab10
Давайте вместо простого и понятного вызова конструктора родим дополнительную абстракцию с генериками и поржем над нубасами, которые пытаются въехать в проект. То, что язык дает такие возможности это прекрасно, но не забывайте про бритву. Подберите более релевантный пример, пожалуйста :)
moonster Автор
Мой реальный кейс, где я впервые применил механизм, вам бы понравился.
Был у меня тест с таким сценарием:
1 - создаем N ссылок на файлы
2 - раскладываем ссылки в специальную структуру
3 - применяем команду к структуре
4 - проверяем, как структура изменилась
Свойства ссылки в рамках структуры зависит места, которое она занимает в структуре, но при этом сама ссылка никакой информации о своем месте не содержит (и не должна).
Ссылок много. Ну и в целом тест оказался не очень простой - код занял полтора экрана.
Для упрощения читаемости теста имена переменных ссылок на файлы я сделал говорящими. Но этого оказалось мало, т.к. при дебаге в дебрях тестируемого кода имена исходных переменных уже недоступны. Поэтому я продублировал имя переменной в поле ссылки вручную, а потом и придумал, как это делать автоматически.
Но это очень специфический случай, вникать в него вряд ли интересно. Поэтому я привел вырожденный пример чтоб проиллюстрировать механизм. В каких реальных случаях его стоит применять - то личное дело каждого. )