Kotlin M5.3: Delegated Properties и не только

от автора

Не так давно мы выпустили очередной майлстоун язвка программировани 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 

ссылка на оригинал статьи http://habrahabr.ru/company/JetBrains/blog/183444/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *