English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Delegación de Kotlin

委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。

Kotlin 直接支持委托模式,更加优雅,简洁。Kotlin 通过关键字 by 实现委托。

类委托

类的委托即一个类中定义的方法实际是调用另一个类的对象的方法来实现的。

以下示例中派生类 Derived 继承了接口 Base 所有方法,并且委托一个传入的 Base 类的对象来执行这些方法。

// 创建接口
interface Base {   
    fun print()
}
// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}
// 通过关键字 by 建立委托类
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
    val b = BaseImpl(10
    Derived(b).print() // Salida 10
}

En la declaración Derived, la cláusula by indica que se guarda b en el ejemplo interno de la clase Derived, y el compilador generará todos los métodos que heredan de la interfaz Base y redirigirá las llamadas a b.

Delegación de propiedades

La delegación de propiedades se refiere a que el valor de una propiedad de una clase no se define directamente en la clase, sino que se delega a una clase agente, lo que permite la gestión unificada de las propiedades de la clase.

Formato de la sintaxis de delegación de propiedades:

val/var <nombre_de_la_propiedad>: <tipo> by <expresión>
  • var/val: tipo de la propiedad (modificable/Sólo lectura)

  • Nombre de la propiedad: nombre de la propiedad

  • Tipo: tipo de datos de la propiedad

  • Expresión: clase delegada

La expresión después de la palabra clave by es la delegación, el método get() (y set() para propiedades var) se delega a los métodos getValue() y setValue() de este objeto. La delegación de propiedades no debe implementar ninguna interfaz, pero debe proporcionar la función getValue() (para propiedades var, también se necesita la función setValue()).

Definir una clase delegada

Esta clase debe contener los métodos getValue() y setValue(), y el parámetro thisRef debe ser el objeto de la clase para la que se realiza la delegación, prop debe ser el objeto de la propiedad para la que se realiza la delegación.

import kotlin.reflect.KProperty
// Definir una clase que contiene delegación de propiedades
class Example {	
    var p: String by Delegate()
}
// Clase delegada
class Delegate {	
    operator fun getValue(thisRef: Any?, property: KProperty<*>):	String {	
        return "$thisRef, aquí se delega la propiedad ${property.name}"
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>,	value: String) {	
        println("$thisRef, la propiedad ${property.name} se asigna el valor $value")
    }
}
fun main(args: Array<String>) {
    val e = Example()
    println(e.p)     // Acceder a esta propiedad, llamar a la función getValue()
    e.p = "w3codebox"   // Llamar a la función setValue()
    println(e.p)
}

El resultado de la salida es:

Example@433c675d, here delegates the p property
Example@433c675La propiedad p del tipo d se asigna el valor w3codebox
Example@433c675d, here delegates the p property

Standard delegation

Kotlin's standard library already contains many factory methods to implement property delegation.

Lazy properties

lazy() is a function that takes a Lambda expression as a parameter and returns a function that returns a Lazy<T> example, which can be used as a delegate to implement lazy properties: The first call to get() will execute the lambda expression passed to lazy() and record the result, and subsequent calls to get() will just return the recorded result.

val lazyValue: String by lazy {
    println("computed!")     // First call output, second call does not execute
    "Hello"
}
fun main(args: Array<String>) {
    println(lazyValue)   // First execution, execute the expression twice
    println(lazyValue)   // Second execution, only output the return value
}

Ejecutar resultados de salida:

computed!
Hello
Hello

Observable properties

Observable can be used to implement the observer pattern.

The Delegates.observable() function accepts two parameters: the first is the initial value, and the second is the property value change event handler.

After the property assignment, an event handler is executed, which has three parameters: the assigned property, the old value, and the new value:

import kotlin.properties.Delegates
class User {
    var name: String by Delegates.observable("Initial value") {
        prop, old, new ->
        println("Old value: $old -> New value: $new
    }
}
fun main(args: Array<String>) {
    val user = User()
    user.name = "First assignment"
    user.name = "Second assignment"
}

Ejecutar resultados de salida:

Old value: Initial value -> New value: First assignment
Old value: First assignment -> New value: Second assignment

Store properties in the map

A common use case is to store property values in a map. This often appears in applications like parsing JSON or doing other "dynamic" things. In this case, you can use the map example itself as a delegate to implement delegate properties.

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val url: String by map
}
fun main(args: Array<String>) {
    // Constructor accepts a mapping parameter
    val site = Site(mapOf(
        "name" to "Sitio web básico",
        "url" to "www.w"3codebox.com"
    ))
    
    // Lectura de valores de mapeo
    println(site.name)
    println(site.url)
}

Ejecutar resultados de salida:

Sitio web básico
es.oldtoolbag.com

Si se utiliza la propiedad var, es necesario cambiar Map por MutableMap:

class Site(val map: MutableMap<String, Any?>) {
    val name: String by map
    val url: String by map
}
fun main(args: Array<String>) {
    var map: MutableMap<String, Any?> = mutableMapOf(
            "name" to "Sitio web básico",
            "url" to "es.oldtoolbag.com"
    
    val site = Site(map)
    println(site.name)
    println(site.url)
    println("--------------
    map.put("name", "Google")
    map.put("url", "www.google.com")
    println(site.name)
    println(site.url)
}

Ejecutar resultados de salida:

Sitio web básico
es.oldtoolbag.com
--------------
Google
www.google.com

NotNull

NotNull es adecuado para aquellos casos en los que no se puede determinar el valor de la propiedad en la fase de inicialización.

class Foo {
    var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)

Es importante destacar que si la propiedad se accede antes de que se asigne, se lanzará una excepción.

Propiedades delegadas locales

Puedes declarar variables locales como propiedades delegadas. Por ejemplo, puedes inicializar una variable local de manera perezosa:

fun example(computeFoo: () -> -> Foo) {
    val memoizedFoo by lazy(computeFoo)
    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

La variable memoizedFoo solo se calculará en la primera visita. Si someCondition falla, esta variable no se calculará en absoluto.

Requisitos de delegación de propiedades

Para las propiedades de solo lectura (es decir, la propiedad val), su delegado debe proporcionar una función llamada getValue(). Esta función acepta los siguientes parámetros:

  • thisRef —— debe ser el mismo tipo que el propietario del atributo (para atributos de extensión, se refiere al tipo extendido) o un supertipo

  • property —— debe ser del tipo KProperty o un supertipo

Esta función debe retornar el mismo tipo (o un subtipo) que el atributo.

Para un atributo mutable (es decir, var), además de la función getValue(), su delegado debe proporcionar otra función llamada setValue(), que acepta los siguientes parámetros:

  • thisRef —— debe ser el mismo tipo que el propietario del atributo (para atributos de extensión, se refiere al tipo extendido) o un supertipo

  • property —— debe ser del tipo KProperty o un supertipo

  • new value —— debe ser del mismo tipo que el atributo o un supertipo.

Reglas de compilación

Detrás de cada implementación de atributo delegado, el compilador de Kotlin genera propiedades auxiliares y las delega. Por ejemplo, para el atributo prop, se genera la propiedad oculta prop$delegate, y el código del acceso solo delega en esta propiedad adicional:

class C {
    var prop: Type by MyDelegate()
}
// Este es el código correspondiente generado por el compilador:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

El compilador de Kotlin proporciona toda la información necesaria sobre prop en los parámetros: el primer parámetro this se refiere al ejemplo externo de la clase C y this::prop es un objeto de reflexión del tipo KProperty, que describe prop mismo.

Proporcionar delegación

Al definir el operador provideDelegate, se puede expandir la lógica de delegación del objeto delegado en la implementación de creación de atributos. Si el objeto utilizado a la derecha del operador by define provideDelegate como un miembro o una función de extensión, se llamará a esta función para crear un ejemplo de delegación de atributo.

Una posible aplicación de provideDelegate es verificar la consistencia del atributo al crearlo (y no solo en su getter o setter).

por ejemplo, si se desea verificar el nombre del atributo antes de la asociación, se puede escribir así:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // Crear delegado
    }
    private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }
class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

Los parámetros de provideDelegate son los mismos que los de getValue:

  • thisRef - debe ser el mismo tipo que el del propietario de la propiedad (para las propiedades de extensión, se refiere al tipo que se extiende)

  • property - debe ser del tipo KProperty o su superclase.

Durante la creación del ejemplo de MyUI, llame al método provideDelegate para cada propiedad y ejecute inmediatamente la verificación necesaria.

Si no se tiene la capacidad de interceptar la asociación entre la propiedad y su delegado, para lograr la misma funcionalidad, debe pasar explícitamente el nombre de la propiedad, lo que no es muy conveniente:

// Verificar el nombre de la propiedad sin usar la función "provideDelegate"
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)
   // Crear delegado
}

En el código generado, se llama al método provideDelegate para inicializar la propiedad auxiliar prop$delegate. Compare el código generado para la declaración de propiedad val prop: Type by MyDelegate() con el código generado anteriormente (cuando el método provideDelegate no existe):

class C {
    var prop: Type by MyDelegate()
}
// Este código se ejecuta cuando la función "provideDelegate" está disponible
// Código generado por el compilador:
class C {
    // Llame a 'provideDelegate' para crear propiedades 'delegate' adicionales
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

Tenga en cuenta que el método provideDelegate solo afecta la creación de propiedades auxiliares y no afecta el código generado para los getters o setters.