Kotlin Learning Series: delegation and its principles

Class delegate

Class delegate actually corresponds to the proxy pattern in Java

interface Base{
    fun text()
}

//Delegated class (real class)
class BaseImpl(val x :String ) : Base{
    override fun text() {
        println(x)
    }
}

//Principal (agent)
class Devices (b :Base) :Base by b


fun main(){
    var b = BaseImpl("I am a real class")
    Devices(b).text()
}

output

 I am a real class

You can see that the delegate class (proxy class) holds the object of the real class, and then the delegate class (proxy class) calls the method with the same name of the real class. Finally, the real class that really implements the method is the real class, which is actually the proxy mode

The delegate implementation in kotlin relies on the by keyword. The by keyword is followed by the delegate class, which can be an expression

Decompile into java code

public final class Devices implements Base {
   // $FF: synthetic field
   private final Base $$delegate_0;

   public Devices(@NotNull Base b) {
      Intrinsics.checkNotNullParameter(b, "b");
      super();
      this.$$delegate_0 = b;
   }

   public void text() {
      this.$$delegate_0.text();
   }
}



public final class BaseImpl implements Base {
   @NotNull
   private final String x;

   public void text() {
      String var1 = this.x;
      boolean var2 = false;
      System.out.println(var1);
   }

   @NotNull
   public final String getX() {
      return this.x;
   }

   public BaseImpl(@NotNull String x) {
      Intrinsics.checkNotNullParameter(x, "x");
      super();
      this.x = x;
   }
}


public interface Base {
   void text();
}


public final class BaseKt {
   public static final void main() {
      BaseImpl b = new BaseImpl("I am a real class");
      (new Devices((Base)b)).text();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

You can see that it is actually the proxy mode. Devices holds the BaseImpl object, rewrites the text method, and calls BaseImpl.text() internally

Attribute delegate

Property delegation is the same as class delegation. Property delegation is actually a delegation to the set/get method of the property, which delegates the set/get method to the setValue/getValue method. Therefore, the delegated class (real class) needs to provide setValue/getValue method, and the val property only needs to provide setValue method

Attribute delegate syntax:

Val / var < attribute name >: < type > by < expression >

class B {
    //Delegate properties
    var a : String by Text()
}

//Delegated class (real class)
class Text {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Property owner = $thisRef, The name of the property = '${property.name}' The value of the property "
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("The value of the property = $value The name of the property =  '${property.name}' Property owner =  $thisRef")
    }
}

fun main(){
    var b = B()

    println(b.a)

    b.a = "ahaha"
    
}

output

Property owner = com.example.lib.weituo.B@27fa135a, The name of the property = 'a' The value of the property 
The value of the property = ahaha The name of the property =  'a' Property owner =  com.example.lib.weituo.B@27fa135a

As can be seen from the above example, attribute a is delegated to Text, and there are setValue and getValue in the Text class. Therefore, when we call the get/set method of attribute a, it will be delegated to setValue/getValue of Text. As can be seen from the above example, there are several parameters to introduce:

thisRef: the owner of the attribute

Property: the description of the property, which is kproperty < * > type or parent class

Value: the value of the attribute

Decompile into Java code

public final class B {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(B.class, "a", "getA()Ljava/lang/String;", 0))};
   @NotNull
   private final Text a$delegate = new Text();

   @NotNull
   public final String getA() {
      return this.a$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setA(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.a$delegate.setValue(this, $$delegatedProperties[0], var1);
   }
}


public final class BKt {
   public static final void main() {
      B b = new B();
      String var1 = b.getA();
      boolean var2 = false;
      System.out.println(var1);
      b.setA("ahaha");
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}


public final class Text {
   @NotNull
   public final String getValue(@Nullable Object thisRef, @NotNull KProperty property) {
      Intrinsics.checkNotNullParameter(property, "property");
      return "Property owner = " + thisRef + ", The name of the property = '" + property.getName() + "' The value of the property ";
   }

   public final void setValue(@Nullable Object thisRef, @NotNull KProperty property, @NotNull String value) {
      Intrinsics.checkNotNullParameter(property, "property");
      Intrinsics.checkNotNullParameter(value, "value");
      String var4 = "The value of the property = " + value + " The name of the property =  '" + property.getName() + "' Property owner =  " + thisRef;
      boolean var5 = false;
      System.out.println(var4);
   }
}

You can see that class B holds a Text object. When calling the B.get() method, Text. GetValue () is called internally, and KProperty is created in B to save various parameters of the property

Simpler implementation of attribute delegation

Each time we implement a delegate, we need to write getValue/setValue methods, which is troublesome. The system provides us with an interface to rewrite these methods, ReadOnlyProperty and ReadWriteProperty

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

The delegated class only needs to implement the interface rewrite method, and val inherits ReadOnlyProperty

class Text1 : ReadOnlyProperty<Any, String> {
    override fun getValue(thisRef: Any, property: KProperty<*>): String {
        return "Property owner = $thisRef, The name of the property = '${property.name}' The value of the property "
    }
}

class Text2 : ReadWriteProperty<Any, String> {
    override fun getValue(thisRef: Any, property: KProperty<*>): String {
        return "Property owner = $thisRef, The name of the property = '${property.name}' The value of the property "
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
        println("The value of the property = $value The name of the property =  '${property.name}' Property owner =  $thisRef")

    }

}

class B {

    val b :String by Text1()

    val c :String by Text2()

}

Several delegates provided in the Koltin standard library

  • Delay properties: its value is calculated only when accessing
  • observable properties: the listener will receive the change notification of this property
  • Map multiple attributes into a (Map) instead of having a single field

Delay attribute lazy

lazy() receives a lambda and returns a lazy instance. The returned instance can be used as a delegate to implement the delay attribute. It is initialized only when the attribute is called for the first time

class Lazy {
    val name :String by lazy {
        println("First call initialization")
        "aa" }
}

fun main(){
    var lazy =Lazy()
    println(lazy.name)
    println(lazy.name)
    println(lazy.name)
}

output

First call initialization
aa
aa
aa

Decompile Java code

public final class Lazy {
   @NotNull
   private final kotlin.Lazy name$delegate;

   @NotNull
   public final String getName() {
      kotlin.Lazy var1 = this.name$delegate;
      Object var3 = null;
      boolean var4 = false;
      return (String)var1.getValue();
   }

   public Lazy() {
      this.name$delegate = kotlin.LazyKt.lazy((Function0)null.INSTANCE);
   }
}
// LazyKt.java

public final class LazyKt {
   public static final void main() {
      Lazy lazy = new Lazy();
      String var1 = lazy.getName();
      boolean var2 = false;
      System.out.println(var1);
      var1 = lazy.getName();
      var2 = false;
      System.out.println(var1);
      var1 = lazy.getName();
      var2 = false;
      System.out.println(var1);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

You can see that the name$delegate variable generated during the reinitialization of lazy is of type kotlin.Lazy, and the value returned by the getName() method is actually name$delegate.getValue()

name$delegate is generated by kotlin.LazyKt.lazy((Function0)null.INSTANCE). Let's look at the source code

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

Finally, it is generated by synchronized lazyimpl. Continue to follow the source code

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

We can directly look at the get method of value if_ v1 !== UNINITIALIZED_VALUE indicates that it has been initialized and returns value directly. Otherwise, it indicates that it has not been initialized. Call the initializer method, that is, the lambda expression of lazy

In fact, it is similar to the delegate implemented by yourself. It also implements the getValue method

lazy delegate parameters

public enum class LazyThreadSafetyMode {

    /**
     * Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
     */
    SYNCHRONIZED,

    /**
     * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
     * but only the first returned value will be used as the value of [Lazy] instance.
     */
    PUBLICATION,

    /**
     * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
     *
     * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
     */
    NONE,
}
  • SYNCHRONIZED: add synchronization lock to make lazy delay initialization thread safe
  • PUBLICATION: the initialized lambda expression can be called multiple times at the same time, but only the first return value is used as the initialization value
  • NONE: no synchronization lock, non thread safe
    val name :String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        println("First call initialization")
        "aa" }
}

Observable attribute observable delegate

You can observe the change process of an attribute

class Lazy {
   
    var a : String by Delegates.observable("Default value"){
            property, oldValue, newValue ->

        println( "$oldValue -> $newValue ")
    }
}

fun main(){
    var lazy =Lazy()


    lazy.a = "First modification"

    lazy.a = "Second modification"
}

output

Default value -> First modification 
First modification -> Second modification 

vetoable delegate

Like Observable, vetoable can observe the changes of attributes. The difference is that vetoable can decide whether to use new values

class C {
    var age: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        println("oldValue = $oldValue -> oldValue = $newValue" )
        newValue > oldValue


    }
}

fun main() {
    var c = C()

    c.age = 5
    println(c.age)

    c.age = 10
    println(c.age)

    c.age = 8
    println(c.age)

    c.age = 20
    println(c.age)
}

output

oldValue = 0 -> oldValue = 5
5
oldValue = 5 -> oldValue = 10
10
oldValue = 10 -> oldValue = 8
10
oldValue = 10 -> oldValue = 20
20

When the new value is less than the old value, it will not take effect. You can see that the value set for the third time is 8, and if it is less than 10, it will not take effect

Properties are stored in the Map

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

fun main(){
    var d = D(mapOf(
        "name" to "Xiao Ming",
        "age" to 12
    ))


    println("name = ${d.name},age = ${d.age}")


}

output

name = Xiao Ming,age = 12

Tags: Android kotlin

Posted on Thu, 07 Oct 2021 22:17:26 -0400 by x_filed2k