Не так давно мы выпустили очередной майлстоун языка программировани Kotlin, M5.3.
В этот релиз вошло довольно много разных изменений: от рефакторингов до новых возможностей в языке. Здесь я хочу рассказать про самое интересное изменение: поддержку делегированных свойств (delegated properties).
Многим хочется, чтобы язык поддерживал
В принципе, можно всем этим людям сказать, мол, слишком много хотите, а жизнь, мол, тяжела… Другой вариант: каждый вид свойств поддержать в языке специальным образом… Лично мне не нравятся оба варианта: печальные пользователи навевают уныние, а заводить специальную поддержку на каждый чих — очень накладно при разработке языка. Так что мы выбрали третий путь: разработали обобщенный механизм, который позволяет выразить разные виды свойств как обычные библиотечные классы, без необходимости каждый из них отдельно поддерживать в языке.
Начнем с примера:
Появился новый синтаксис: теперь после типа свойства можно написать «by <выражение>». Выражение после «by» является делегатом: вызовы геттера и сеттера для этого свойства будут делегированы значению этого выражения. Мы не требуем, чтобы делегат реализовывал какой-то интерфейс, достаточно, чтобы у него были функции get() и set() с определенной сигнатурой:
(Некоторых пугает отсутствие требования реализовывать интерфейс. Не бойтесь, если вам так спокойнее, вот он, даже два — реализуйте :) )
Если мы читаем значение свойства p, вызывается функция get() из класса Delegate, причем первым параметром ей передается тот объект, у которого запрашивается свойство, а вторым — объект-описание самого свойства p (у него можно, в частности, узнать имя свойства):
Этот пример выведет «Example@33a17727, thank you for delegating ‘p’ to me!».
Аналогично, когда присходит запись свойства, вызывается set(). Два первых параметра — такие же как у get(), а третий — присваиваемое значение свойства:
Этот пример выведет «NEW has been assigned to ‘p’ in Example@33a17727».
Вы, наверное, уже догадались, как можно реализовать ленивые свойства и пр.? Можете попробовать сделать это сами, но бОльшая часть всего этого уже реализована в стандартной библиотеке Kotlin. Наиболее употребительные делегаты определены в объекте kotlin.properties.Delegates.
Начнем с lazy:
Функция Delegates.lazy() возвращает объект-делегат, реализующий ленивое вычисление значения свойства: первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение; последующие вызовы просто возвращают запомненное.
Если Вы хотите использовать ленивые свойства в многопоточной программе, воспользуйтесь функцией blockingLazy(): она гарантирует, что значение будет вычислено ровно одним потоком и корректно опубликовано.
Функция observable() принимает два аргумента: начальное значение свойства и обработчик (лямбда-выражение), который вызывается при каждом присваивании. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение. Если Вам нужно иметь возможность запретить присваивание некоторых значений, используйте функцию vetoable() вместо observable().
Относительно неожиданное применение делегатов: многие пользователи спрашивают: «Как объявить not-null свойство, если у меня нет значения, которым его проинициализировать (я его потом присвою)?». Kotlin не разрешает объявлять неабстрактные свойства без инициализаторов:
Можно было бы присвоить null, то тогда тип будет уже не «Bar», а «Bar?», и при каждом обращении нужно будет обрабатывать случай нулевой ссылки… Теперь можно обойтись делегатом:
Если это свойство считать до первого присваивания, делегат бросит исключение. После инициализации он просто возвращает ранее записанное значение.
Последний пример из библиотеки: хранение свойств в Map. Это полезно в «динамическом» коде, например, при работе с JSON:
Конструктор этого класса принимает map:
Делегаты вычисляют значения по стоковым ключам — именам свойств:
Изменяемые свойства (var) поддержиаются с помощью функции mapVar(): значения записываются по таким же ключам (для этого нужен MutableMap, а не просто Map).
Мы поговорили о делегированных свойствах, механизме, который добавляет новую степень свободы в язык и дает возможность задавать свою семантику операциям над свойствами. Я показал примеры, лежащие на поверхности, но, наверняка, есть еще немало способов применить этот механизм, так что добро пожаловать: придумывайте и реалиуйте!
P.S. Про другие новинки в Kotlin M5.3 можно почитать здесь (по-английски).
В этот релиз вошло довольно много разных изменений: от рефакторингов до новых возможностей в языке. Здесь я хочу рассказать про самое интересное изменение: поддержку делегированных свойств (delegated properties).
Многим хочется, чтобы язык поддерживал
- ленивые свойства (lazy properties): значение вычисляется один раз, при первом обращении;
- свойства, на события об изменении которых можно подписаться (observable properties);
- свойства, хранимые в Map'е, а не в отдельных полях;
- <еще вот такие крутые свойства>...
В принципе, можно всем этим людям сказать, мол, слишком много хотите, а жизнь, мол, тяжела… Другой вариант: каждый вид свойств поддержать в языке специальным образом… Лично мне не нравятся оба варианта: печальные пользователи навевают уныние, а заводить специальную поддержку на каждый чих — очень накладно при разработке языка. Так что мы выбрали третий путь: разработали обобщенный механизм, который позволяет выразить разные виды свойств как обычные библиотечные классы, без необходимости каждый из них отдельно поддерживать в языке.
Делегированные свойства
Начнем с примера:
class Example {
var p: String by Delegate()
}
Появился новый синтаксис: теперь после типа свойства можно написать «by <выражение>». Выражение после «by» является делегатом: вызовы геттера и сеттера для этого свойства будут делегированы значению этого выражения. Мы не требуем, чтобы делегат реализовывал какой-то интерфейс, достаточно, чтобы у него были функции get() и set() с определенной сигнатурой:
class Delegate() {
fun get(thisRef: Any?, prop: PropertyMetadata): String {
return "$thisRef, thank you for delegating '${prop.name}' to me!"
}
fun set(thisRef: Any?, prop: PropertyMetadata, value: String) {
println("$value has been assigned")
}
}
(Некоторых пугает отсутствие требования реализовывать интерфейс. Не бойтесь, если вам так спокойнее, вот он, даже два — реализуйте :) )
Если мы читаем значение свойства p, вызывается функция get() из класса Delegate, причем первым параметром ей передается тот объект, у которого запрашивается свойство, а вторым — объект-описание самого свойства p (у него можно, в частности, узнать имя свойства):
val e = Example()
println(e.p)
Этот пример выведет «Example@33a17727, thank you for delegating ‘p’ to me!».
Аналогично, когда присходит запись свойства, вызывается set(). Два первых параметра — такие же как у get(), а третий — присваиваемое значение свойства:
e.p = "NEW"
Этот пример выведет «NEW has been assigned to ‘p’ in Example@33a17727».
Вы, наверное, уже догадались, как можно реализовать ленивые свойства и пр.? Можете попробовать сделать это сами, но бОльшая часть всего этого уже реализована в стандартной библиотеке Kotlin. Наиболее употребительные делегаты определены в объекте kotlin.properties.Delegates.
Ленивые свойства
Начнем с lazy:
import kotlin.properties.Delegates
class LazySample {
val lazy: String by Delegates.lazy {
println("computed!")
"Hello"
}
}
Функция Delegates.lazy() возвращает объект-делегат, реализующий ленивое вычисление значения свойства: первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение; последующие вызовы просто возвращают запомненное.
Если Вы хотите использовать ленивые свойства в многопоточной программе, воспользуйтесь функцией blockingLazy(): она гарантирует, что значение будет вычислено ровно одним потоком и корректно опубликовано.
Observable свойства
class User {
var name: String by Delegates.observable("<no name>") {
d, old, new ->
println("$old -> $new")
}
}
Функция observable() принимает два аргумента: начальное значение свойства и обработчик (лямбда-выражение), который вызывается при каждом присваивании. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение. Если Вам нужно иметь возможность запретить присваивание некоторых значений, используйте функцию vetoable() вместо observable().
Свойства без инициализаторов
Относительно неожиданное применение делегатов: многие пользователи спрашивают: «Как объявить not-null свойство, если у меня нет значения, которым его проинициализировать (я его потом присвою)?». Kotlin не разрешает объявлять неабстрактные свойства без инициализаторов:
class Foo {
var bar: Bar // error: must be initialized
}
Можно было бы присвоить null, то тогда тип будет уже не «Bar», а «Bar?», и при каждом обращении нужно будет обрабатывать случай нулевой ссылки… Теперь можно обойтись делегатом:
class Foo {
var bar: Bar by Delegates.notNull()
}
Если это свойство считать до первого присваивания, делегат бросит исключение. После инициализации он просто возвращает ранее записанное значение.
Хранение свойств в хеш-таблице
Последний пример из библиотеки: хранение свойств в Map. Это полезно в «динамическом» коде, например, при работе с JSON:
class User(val map: Map<String, Any?>) {
val name: String by Delegates.mapVal(map)
val age: Int by Delegates.mapVal(map)
}
Конструктор этого класса принимает map:
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
Делегаты вычисляют значения по стоковым ключам — именам свойств:
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
Изменяемые свойства (var) поддержиаются с помощью функции mapVar(): значения записываются по таким же ключам (для этого нужен MutableMap, а не просто Map).
Заключение
Мы поговорили о делегированных свойствах, механизме, который добавляет новую степень свободы в язык и дает возможность задавать свою семантику операциям над свойствами. Я показал примеры, лежащие на поверхности, но, наверняка, есть еще немало способов применить этот механизм, так что добро пожаловать: придумывайте и реалиуйте!
P.S. Про другие новинки в Kotlin M5.3 можно почитать здесь (по-английски).