Kotlin topic "21": delegation attributes

Don't complain when you encounter difficulties. Since you can't change the past, try to change the future.

1, Overview

   there are some common attribute types. Although we can implement them manually every time we need them, it is best to implement them in the library at one time. For example:

  • Delay attribute: the value is calculated only during the first access;
  • Observe attribute: the listener will be notified of the change of this attribute;
  • Store attributes in the map instead of storing separate fields for each attribute.

To cover these situations, Kotlin supports delegate attributes:

class School {
    var str: String by Delegate()
}

The syntax is Val / var < property name >: < type > by < expression >. The expression after by is a delegate, because the get() (and set()) corresponding to the property will be delegated to its getValue() and setValue() methods. Property delegate does not need to implement any interface, but must provide a getValue() function (and setValue() - for var). For example:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

When we read the instance entrusted to Delegate from str, the getValue() function will be called, so its first parameter is the object we read from str, and the second parameter contains the description of str itself (for example, you can take its name).

fun test() {
    val school = School()
    println("Delegate attribute: school == ${school.str}")
}

Print data as follows:

Delegate attribute: school == com.suming.kotlindemo.blog.School@8d409f9, thank you for delegating 'str' to me!

Similarly, when we assign a value to str, we call the setValue() function. The first two parameters are the same, and the third saves the assigned value:

fun test() {
    val school = School()
    school.str = "Android"
}

Print data as follows:

Android has been assigned to 'str' in com.suming.kotlindemo.blog.School@8d409f9.

The requirement description of the delegate object can be found below.

Note: due to kotlin1 You can declare a delegate attribute in a function or code block. It should not be a member of a class. Here is an example.

2, Standard entrustment

The Kotlin standard library provides factory methods for several useful delegates:

2.1 lazy loading

lazy() is a function that receives a lambda and returns an instance of lazy < T >, which can be used as a delegate to implement a lazy property: the first call to get() executes the lambda passed to lazy() and remembers the result, and subsequent calls to get() only return the remembered result.

	val lazyValue: String by lazy {
    	println("---lazyValue lazy initialization ---")
    	"Kotlin"
	}

	//call
    fun test() {
        println(lazyValue)
        println(lazyValue)
    }

Print data as follows:

---lazyValue lazy initialization ---
Kotlin
Kotlin

By default, the calculation of the delay attribute is synchronous: the value is calculated in only one thread, and all threads will see the same value. If you do not need to initialize the synchronization of delegates so that multiple threads can execute at the same time, you can use LazyThreadSafetyMode. Publish as a parameter of the lazy() function. If your initialization always occurs on the same thread that you use properties, you can use LazyThreadSafetyMode NONE . It does not introduce any thread guarantees and associated overhead.

2.2 observed

Delegates.observable() has two parameters: an initialization value and a handler for modification. This handler is called every time we assign a value to an attribute (after the assignment is performed). It has three parameters: an assigned attribute, old value and new value:

    class User {
        var name: String by Delegates.observable("no name") { 
        	prop, old, new ->
            println("observable: $old == $new")
        }
    }
    
    fun main() {
        val user = User()
        user.name = "first"
        user.name = "second"
    }

Print data as follows:

observable: no name == first
observable: first == second

If you want to intercept assignments and veto them, use vetoable() instead of observable(). The handler passed to vetoable is called before performing the assignment of the new property value.

2, Delegate to another property

From kotlin1 Since 4, a property can delegate its getter and setter to another property. This delegate can be used for top-level properties and class properties (members and extensions). The delegate attribute can be:

  • A top-level attribute;
  • Members or extended attributes of the same class;
  • Members or extended properties of another class.

To delegate an attribute to another attribute, use the correct qualifier::, such as this::delegate or MyClass::delegate, in the delegate name.

var topLevelInt: Int = 0

class WithDelegate(val numA: Int)

class MyClass(var memberInt: Int, val instance: WithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by instance::numA
}

var MyClass.extDelegated: Int by ::topLevelInt

This can be very useful, for example, when you want to rename attributes in a compatible way in the future: introduce a new attribute, annotate the old attribute with @ Deprecated, and delegate its implementation.

class MyClass {
    var newName: Int = 0

    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: Int by this::newName
}

fun main() {
    val myClass = MyClass()
    // Note: 'oldName: Int' has been deprecated and replaced with 'newName'
    myClass.oldName = 42
    println(myClass.newName) // 42
}

3, Store attributes in Map

A common usage is to store attribute values in a map. This often happens in JSON or other applications. In this case, you can use the mapping instance itself as a delegate for delegate properties.

    class Student(val map: Map<String, Any?>) {
        val name: String by map
        val age: Int by map
    }

In the following example, the constructor has a map:

	val student: Student = Student(mapOf("name" to "Kotlin", "age" to 20))
	println("Student: name == ${student.name}, age == ${student.age}")

The delegate attribute obtains the value from this mapping (through the string - the name of the attribute), and the print data is as follows:

Student: name == Kotlin, age == 20

If you use MutableMap instead of read-only Map, this also applies to the var attribute.

    class MutlStudent(val map: MutableMap<String, Any?>) {
        val name: String by map
        val age: Int by map
    }

4, Local delegate properties

You can declare local variables as delegate properties. For example, you can delay a local variable:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

The variable memoizedFoo is evaluated only on the first access. If someCondition fails, the variable is not evaluated at all.

5, Attribute delegation requirements

Here we summarize the requirements of the delegate object:
For a read-only property (val), the delegate must provide an operation function getValue() with the following parameters.

  • thisRef -- must be the same or super type of the attribute owner (for extended attribute - extended type);
  • property -- must be kproperty < * > type or its supertype.

getValue() must return the same type as the property (or its subtype).

    class Resource

    class Owner {
        val valResource: Resource by ResourceDelegate()
    }

    class ResourceDelegate {
        operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
            return Resource()
        }
    }

For the variable attribute (var), the delegate must additionally provide an operation function setValue() with the following parameters:

  • thisRef -- must be the same or super type of the attribute owner (for extended types);
  • property -- must be kproperty < * > or its supertype;
  • value -- must be the same type as the attribute (or its supertype).
    class Resource

    class Owner {
        val valResource: Resource by ResourceDelegate()
    }

    class ResourceDelegate(private var resource: Resource = Resource()) {
        operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
            return Resource()
        }

        operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
            if (value is Resource) {
                resource = value
            }
        }
    }

getValue() and, or setValue() functions can be provided as member functions or extension functions of delegate classes. The latter is convenient when you need to delegate properties to objects that do not initially provide these functions. Both functions need to be marked with the operator keyword.

You can create delegates as anonymous objects without using the ReadOnlyProperty and ReadWriteProperty interfaces in the Kotlin standard library to create new classes. They provide the necessary methods: getValue() is declared in ReadOnlyProperty, which extends it and adds setValue(). Therefore, you can pass ReadWriteProperty whenever you need ReadOnlyProperty.

    fun resourceDelegate(): ReadWriteProperty<Any?, Int> = object : ReadWriteProperty<Any?, Int> {
        var curValue = 0
        override fun getValue(thisRef: Any?, property: KProperty<*>): Int = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            curValue = value
        }
    }

    val readOnly: Int by resourceDelegate()//ReadWriteProperty is val's
    val readWrite: Int by resourceDelegate()

Translation rules

At the bottom, the Kotlin compiler generates an auxiliary attribute for each delegate attribute and delegates it. For example, for the attribute prop, the hidden attribute prop$delegate will be generated, and the accessor code will simply delegate to this additional attribute:

    class Person {
        var prop: Int by Delegate()
    }

    // The following code is generated by the compiler
    class Person  {
        private val prop$delegate = Delegate()
        var prop: Int
            get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
    }

The Kotlin compiler provides all necessary information about prop in the parameters: the first parameter this refers to an instance of the external class Person. This:: prop is a reflection object of KProperty type, which describes prop itself.

Note: the binding callable this::prop syntax in the direct reference code is only available in kotlin1 It can only be used after 1.

Provide a delegate

By defining the provideDelegate operator, you can expand the logic of creating attributes and implementing delegates to objects. If the object used on the right side of by defines provideDelegate as a member or extension function, this function will be called to create a property delegate instance.

One of the most likely uses of provideDelegate is to check the consistency of properties during initialization. For example, if you want to check the property name before binding, you can write:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // Create delegate
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

The parameters of provideDelegate are the same as getValue:

  • thisRef - must be the same or super type of the attribute owner (for extensibility - extended type);
  • property -- must be kproperty < * > or its supertype;

During the creation of the MyUI instance, the provideDelegate method is called for each property, which immediately performs the necessary validation. If you don't have the ability to intercept the binding between attributes and their delegates, to achieve the same function, you must explicitly pass the attribute name, which is not very convenient:

// There is no "provideDelegate" function to check the property name
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
   checkProperty(this, propertyName)
   // create delegate
}

In the generated code, the provideDelegate method will be called to initialize the prop$delegate attribute of the auxiliary. Compare the code generated for the attribute declaration val prop: Type by MyDelegate() (when provideDelegate method does not exist):

class User {
    var prop: Type by MyDelegate()
}

// This code is generated by the compiler
// When the 'provideDelegate' function is available:
class User  {
    //Call 'provideDelegate' to create an additional 'delegate' attribute
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
    set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Note that the provideDelegate method only affects the creation of auxiliary properties, not the code generated for getter s or setter s.

Using the PropertyDelegateProvider interface in the standard library, you can create a delegate provider without creating a new class.

val provider = PropertyDelegateProvider { thisRef: Any?, property ->
    ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}

val delegate: Int by provider

So far, this article ends!


Source address: https://github.com/FollowExcellence/KotlinDemo-master

Please respect the copyright of the creator and indicate the source of Reprint: https://blog.csdn.net/m0_37796683/article/details/109745526 thank you!

Tags: kotlin delegate

Posted by coverman on Fri, 06 May 2022 02:34:53 +0300