Kotlin vocal | kotlin built-in agent

Posted by polandsprings on Tue, 16 Nov 2021 11:08:48 +0100

Agents can help you delegate tasks to other objects, resulting in better code reusability Our previous article Learn more. Kotlin not only allows you to easily implement agents through the by keyword, but also provides built-in agents such as lazy(), observable(), vetoable(), and notNull() in the standard library. Next, let's begin to understand the use of these built-in agents and their internal implementation principles.

lazy()

lazy() The function is a property broker that helps you initialize properties the first time you access them. This function is useful when creating expensive objects.

The lazy() function takes two parameters, the LazyThreadSafetyMode enumeration value and a lambda expression.

LazyThreadSafetyMode is used to specify how the initialization process is synchronized between different threads. Its default value is LazyThreadSafetyMode.SYNCHRONIZED. This means that initialization is thread safe, but at the expense of explicit synchronization, there is a slight impact on performance.

The lambda expression is executed the first time the property is accessed, and its value is stored for subsequent access.

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person(name: String, lastname: String) {
   val fullname: String by lazy() {
     name + lastname
   }
   //...
}

Internal principle

When viewing the decompiled Java code, we can see that the Kotlin compiler creates a reference of Lazy type for Lazy proxy:

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

@NotNull
private final Lazy fullname$delegate;

This agent initializes by calling the LazyKt.lazy() function and passing in the lambda expression and thread safe mode parameters you defined:

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

  this.fullname$delegate = LazyKt.lazy((Function0)(new Function0() {
     @NotNull
     public final String invoke() {
        return name + lastname;
     }
  }));

Let's observe lazy() Source code. Since the lazy() function uses the LazyThreadSafetyMode.SYNCHRONIZED parameter by default, it will return a synchronized lazyimpl type Lazy object:

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

When the property agent is accessed for the first time, the getValue() function of synchronized lazyimpl will be called. This function will initialize the property in a synchronized block:

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

This ensures that lazy objects are initialized in a thread safe manner, but also introduces the additional overhead caused by synchronized blocks.

Note: if you are sure that the resource will be initialized in a single thread, you can pass LazyThreadSafetyMode.NONE to lazy(), so that the function will not use the synchronized block during lazy initialization. Remember, however, that lazythreadsafetymode. None does not change the synchronization characteristics of lazy initialization. Because lazy initialization is synchronous, it will still consume the same time as the non lazy initialization process on the first access, which means that objects with more time-consuming initialization process will still block the UI thread when accessed.

1val lazyValue: String by lazy(LazyThreadSafetyMode.NONE) {"lazy"}

Lazy initialization can help initialize expensive resources, but for simple objects such as String, the lazy() function needs to generate additional objects such as lazy and KProperty, which will increase the overhead of the whole process.

Observable

Delegates.observable() Is another built-in agent in the Kotlin standard library. The first mock exam is a design pattern. In this model, an object will maintain a list of its subordinates, who are called observers. The object notifies the observer when its own state changes. The first mock exam is very suitable for the notification of a certain value when the value changes. It can avoid the realization of calling and checking whether the resource is updated for the subordinate object cycle.

The observable () function receives two parameters: the initialization value and a listening processor that will be called when the value changes. observable() creates a ObservableProperty Object to execute the lambda expression you pass to the proxy each time the setter is called.

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person {
    var address: String by Delegates.observable("not entered yet!") {
        property, oldValue, newValue ->
        // Perform update operation
    }
}

By looking at the decompiled Person type, we can see that the Kotlin compiler generates an inheritance ObservableProperty Class. This class also implements a function called afterChange(), which holds the lambda function you passed to the observable agent.

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

protected void afterChange(@NotNull KProperty property, Object oldValue, Object newValue) {
               // Perform update operation
}

The afterChange() function is called by the setter of the parent ObservableProperty class, which means that whenever the caller sets a new value for address, the setter will automatically call the afterChange() function. As a result, all listeners will be notified of the change.

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person {
    var address: String by Delegates.observable("not entered yet!") {
        property, oldValue, newValue ->
        // Perform update operation
    }
}

You can also see the call of the beforeChange() function from the decompiled code, and the observable agent will not use beforeChange(). But for vetoable(), which you will see next, this function is the basis of the function implementation.

vetoable

vetoable() Is a built-in agent, and the property delegates the veto to its value. Like the observable() proxy, vetoable() also accepts two parameters: the initial value and the listener. When any caller wants to modify the property value, the listener will be called.

<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

var address: String by Delegates.vetoable("") {
       property, oldValue, newValue ->
   newValue.length > 14
}

If the lambda expression returns true, the property value will be modified, otherwise it will remain unchanged. In this case, if the caller tries to update the address with a string less than 15 characters in length, the current value will not change.

Observing the decomposed Person, we can see that Kotlin has generated a new class that inherits ObservableProperty, which contains the lambda expression that we import to beforeChange() function. setter will call lambda expression before each value is set.

 <!-- Copyright 2020 Google LLC.
 SPDX-License-Identifier: Apache-2.0 -->
 
 public final class Person$$special$$inlined$vetoable$1 extends ObservableProperty {
 
   protected boolean beforeChange(@NotNull KProperty property, Object oldValue, Object newValue) {
      Intrinsics.checkParameterIsNotNull(property, "property");
      String newValue = (String)newValue;
      String var10001 = (String)oldValue;
     int var7 = false;
     return newValue.length() > 14;
  }
}

notNull

The last built-in agent provided in the Kotlin standard library is Delegates.notNull() . notNull() allows a property to be initialized later, and lateinit similar. Since notNull() creates additional objects for each property, lateinit is recommended in most cases. However, you can use notNull() with native types, which lateinit does not support.

val fullname: String by Delegates.notNull<String>()

notNull() uses a special type of ReadWriteProperty: NotNullVar.

We can view the decompiled code. In the following example, the fullname attribute is initialized with notNull() function:

this.fullname$delegate = Delegates.INSTANCE.notNull();

The function returned a NotNullVar object:

public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()

NotNullVar type holds a generic nullable internal reference. If any code calls getter before initializing the value, it will throw IllegalStateException().

 <!-- Copyright 2020 Google LLC.
 SPDX-License-Identifier: Apache-2.0 -->
 
 private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
    private var value: T? = null
 
    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

   public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
       this.value = value
   }
}

With this set of built-in agents provided by the Kotlin standard library, you don't have to write, maintain and reinvent these functions. These built-in agents can help you lazy initialize fields, allow native types to delay loading, listen and be notified when values change, and even veto property value changes.

Topics: kotlin