company_banner
15 июня 2013 в 22:44

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

Изменяемые свойства (var) поддержиаются с помощью функции mapVar(): значения записываются по таким же ключам (для этого нужен MutableMap, а не просто Map).

Заключение


Мы поговорили о делегированных свойствах, механизме, который добавляет новую степень свободы в язык и дает возможность задавать свою семантику операциям над свойствами. Я показал примеры, лежащие на поверхности, но, наверняка, есть еще немало способов применить этот механизм, так что добро пожаловать: придумывайте и реалиуйте!

P.S. Про другие новинки в Kotlin M5.3 можно почитать здесь (по-английски).
Автор: @abreslav
JetBrains
рейтинг 273,81
Похожие публикации

Комментарии (7)

  • 0
    А когда все-таки будет релиз?)
    • 0
      Относительно скоро :)
  • 0
    Думаю, в подавляющем большинстве случаев все свойства класса будут использовать один и тот же делегат. Поэтому было бы удобно указывать его один раз на уровне описания класса. Как вариант: если класс имплементирует интерфейс делегата, то он сам является делегатом для своих свойств, а также для свойств всех его потомков. Концепт делегата в этом случае играет роль интерсептора для свойств. Таким образом, мы можем создать целую domain model с заданным функционалом, описав делегат только для базового класса.
    • 0
      Те юзкейсы, на которые мы смотрели, не подтверждают эту гипотезу. Один делегат бывает, когда данные хранятся централизованно (map или база данных), но это не подавляющее большинство случаев.
      • 0
        Этот случай как раз я и имел ввиду, например реализация ORM при помощи делегатов. Или более сложный кейс, когда есть одна модель, и требуется динамически менять ее поведение в зависимости от контекста: на сервере замепить в DB, на транспортном уровне мепить в XML, а на клиенте добавить возможность отслеживать изменения (PropertyChangeListeners). На данный момент это решается достаточно сложно при помощи магии фреймворков, AOP, проксирования, code enhancement, etc… Если добавить базовые возможности AOP в язык, такие как interceptor-ы для свойств, было бы на порядок проще реализовывать подобные решения.
        • +1
          Делегаты позволяют сделать интерсепторы для свойств относительно дешево: вместо каких-нибудь аннотаций указываем делегата и радуемся. Я понимаю, что просто один раз написать на классе «делегировать все сюда» — выглядит здорово, но на самом деле мы не можем в этом случае никакой type-safety гарантировать, а это важно. Так что необходимость писать делегата для каждого свойства — это такой компромисс. А писать там совсем не много.
  • 0
    Чувствую, что делегаты помогли бы сделать поля в которые значения инжектируются (например) спрингом более подходящего для них типа, например readonly и not-null, но что-то не могу пока понять как.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка