company_banner

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 можно почитать здесь (по-английски).
    Метки:
    JetBrains 272,49
    Компания
    Поделиться публикацией
    Комментарии 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, но что-то не могу пока понять как.

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

          Самое читаемое