Kotlin M5.3: Delegated Properties

    Not so long ago we released another milestone of the Kotlin programming language , M5.3.
    This release includes quite a lot of different changes: from refactoring to new features in the language. Here I want to talk about the most interesting change: support for delegated properties (delegated properties).

    Many people want the language to support
    • lazy properties: the value is calculated once, on the first call;
    • properties that you can subscribe to change events for (observable properties);
    • properties stored in Map, and not in separate fields;
    • <still such cool properties> ...

    In principle, you can say to all these people that you want too much, but life, they say, is hard ... Another option: support each type of property in a language in a special way ... Personally, I don’t like both options: sad users evoke despondency, and start special support for every sneeze - it’s very expensive when developing a language. So we chose the third way: we developed a generalized mechanism that allows us to express different types of properties as ordinary library classes, without the need for each of them to be separately supported in the language.

    Delegated Properties


    Let's start with an example:

    class Example {
      var p: String by Delegate()
    }
    

    A new syntax has appeared: now after the property type you can write “by <expression>”. The expression after “by” is a delegate : getter and setter calls for this property will be delegated to the value of this expression. We do not require the delegate to implement any interface, it is enough that it has the get () and set () functions with a certain signature:

    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")
      }
    }
    

    (Some are frightened by the lack of a requirement to implement an interface. Don’t be afraid if you’re so comfortable, here it is , even two - implement it :))

    If we read the value of the p property, the get () function from the Delegate class is called, and the first parameter is passed to it from which the property is requested, and the second is the description object of the property p itself (you can, in particular, find out the name of the property from it):

    val e = Example()
    println(e.p)
    

    This example will output "Example @ 33a17727, thank you for delegating 'p' to me!"

    Similarly, when a property record comes in, set () is called. The first two parameters are the same as get (), and the third is the property value assigned:

    e.p = "NEW"
    

    This example will output "NEW has been assigned to 'p' in Example @ 33a17727."

    You probably already guessed how you can implement lazy properties, etc.? You can try to do it yourself, but most of all this is already implemented in the Kotlin standard library . The most common delegates are defined in the kotlin.properties.Delegates object .

    Lazy properties


    Let's start with lazy:

    import kotlin.properties.Delegates
    class LazySample {
        val lazy: String by Delegates.lazy {
            println("computed!")
            "Hello"
        }
    }
    

    The Delegates.lazy () function returns a delegate object that implements lazy calculation of the property value: the first call to get () starts the lambda expression passed to lazy () as an argument and remembers the received value; subsequent calls simply return the remembered.

    If you want to use lazy properties in a multi-threaded program, use the blockingLazy () function: it ensures that the value will be calculated by exactly one thread and correctly published.

    Observable Properties


    class User {
        var name: String by Delegates.observable("") {
            d, old, new ->
            println("$old -> $new")
        }
    }
    

    The observable () function takes two arguments: the initial value of the property and the handler (lambda expression), which is called with each assignment. The handler has three parameters: a description of the property that is changing, the old value and the new value. If you need to be able to prevent the assignment of certain values, use the vetoable () function instead of observable ().

    Properties without initializers


    Relatively unexpected use of delegates: many users ask: “How to declare a not-null property if I don’t have a value to initialize it (will I assign it later)?”. Kotlin does not allow declaring non-abstract properties without initializers:

    class Foo {
      var bar: Bar // error: must be initialized
    }
    

    If you could assign null, then the type will no longer be “Bar”, but “Bar?”, And with each call you will need to handle the case of a null reference ... Now you can get by with the delegate:

    class Foo {
      var bar: Bar by Delegates.notNull()
    }
    

    If this property is counted before the first assignment, the delegate will throw an exception. After initialization, it simply returns the previously recorded value.

    Storing properties in a hash table


    The last example from the library: storing properties in Map. This is useful in “dynamic” code, for example, when working with JSON:

    class User(val map: Map) {
        val name: String by Delegates.mapVal(map)
        val age: Int     by Delegates.mapVal(map)
    }
    

    The constructor of this class accepts map:

    val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
    ))
    

    Delegates calculate values ​​by stock keys - property names:

    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
    

    Variable properties (var) are supported using the mapVar () function: values ​​are written using the same keys (this requires a MutableMap, not just a Map).

    Conclusion


    We talked about delegated properties, a mechanism that adds a new degree of freedom to the language and makes it possible to set its own semantics for operations on properties. I have shown examples that lie on the surface, but there are probably many more ways to apply this mechanism, so welcome: come up with and implement!

    PS About other new products in Kotlin M5.3 you can read here (in English) .

    Also popular now: