Pull to refresh
0
JetBrains
Делаем эффективные инструменты для разработчиков

Kotlin M5.3: Delegated Properties

Reading time4 min
Views10K
Не так давно мы выпустили очередной майлстоун языка программировани 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 можно почитать здесь (по-английски).
Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments7

Articles

Information

Website
jetbrains.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Чехия