Delegación en Kotlin
Patrón de delegaciónEs una técnica básica en los patrones de diseño de software. En el patrón de delegación, dos objetos participan en el procesamiento de la misma solicitud; el objeto que recibe la solicitud la delega a otro objeto para que la maneje.
Kotlin admite directamente el patrón de delegación, de manera más elegante y concisa. Kotlin implementa la delegación mediante la palabra clave `by`.
Delegación de clase
La delegación de clase significa que los métodos definidos en una clase realmente se implementan llamando a los métodos de un objeto de otra clase.
En el siguiente ejemplo, la clase derivada `Derived` hereda todos los métodos de la interfaz `Base` y delega la ejecución de estos métodos a un objeto de la clase `Base` pasado como parámetro.
// 创建接口
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() // 输出 10
}
En la declaración de `Derived`, la cláusula `by` indica que `b` se almacena internamente en la instancia del objeto `Derived`, y el compilador generará todos los métodos que heredan de la interfaz `Base` y reenviará las llamadas a `b`.
Delegación de propiedades
La delegación de propiedades se refiere a que el valor de cierta propiedad de una clase no se define directamente en la clase, sino que se confía a una clase proxy, logrando así una gestión unificada de las propiedades de esa clase.
Formato de sintaxis de la delegación de propiedades:
val/var <属性名>: <类型> by <表达式>
- `var`/`val`: tipo de propiedad (mutable / de solo lectura)
- nombre de la propiedad: nombre de la propiedad
- tipo: tipo de dato de la propiedad
- expresión: clase delegada
La expresión después de la palabra clave `by` es el delegado. Los métodos `get()` (y `set()`) de la propiedad serán delegados a los métodos `getValue()` y `setValue()` de este objeto. La delegación de propiedades no tiene que implementar ninguna interfaz, pero debe proporcionar la función `getValue()` (y para propiedades `var`, también necesita la función `setValue()`).
Definir una clase delegada
Esta clase debe contener los métodos `getValue()` y `setValue()`, donde el parámetro `thisRef` es el objeto de la clase que realiza la delegación, y `prop` es el objeto de la propiedad delegada.
import kotlin.reflect.KProperty
// 定义包含属性委托的类
class Example {
var p: String by Delegate()
}
// 委托的类
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 属性赋值为 $value")
}
}
fun main(args: Array<String>) {
val e = Example()
println(e.p) // 访问该属性,调用 getValue() 函数
e.p = "Ejemplo" // 调用 setValue() 函数
println(e.p)
}
El resultado de salida es:
Example@433c675d, 这里委托了 p 属性 Example@433c675d 的 p 属性赋值为 Ejemplo Example@433c675d, 这里委托了 p 属性
Delegados estándar
La biblioteca estándar de Kotlin ya incluye muchos métodos de fábrica para implementar la delegación de propiedades.
Propiedad diferida Lazy
`lazy()` es una función que acepta una expresión Lambda como parámetro y devuelve una instancia de `Lazy<T>`. La instancia devuelta puede usarse como delegado para implementar una propiedad diferida: la primera llamada a `get()` ejecuta la expresión lambda pasada a `lazy()` y registra el resultado, las llamadas posteriores a `get()` simplemente devuelven el resultado registrado.
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}
Resultado de salida:
computed! Hello Hello
Propiedad observable Observable
`observable` se puede usar para implementar el patrón observador.
La función `Delegates.observable()` acepta dos parámetros: el primero es el valor inicial, el segundo es el manejador (handler) del evento de cambio del valor de la propiedad.
Después de asignar la propiedad, se ejecuta el manejador (handler) del evento, que tiene tres parámetros: la propiedad asignada, el valor anterior y el valor nuevo:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("初始值") {
prop, old, new ->
println("旧值:$old -> 新值:$new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "第一次赋值"
user.name = "第二次赋值"
}
Resultado de salida:
Valor antiguo: valor inicial -> Nuevo valor: primera asignación Valor antiguo: primera asignación -> Nuevo valor: segunda asignación
Almacenar propiedades en un mapa
Un caso de uso común es almacenar los valores de las propiedades en un mapa. Esto aparece frecuentemente en aplicaciones que analizan JSON u otras cosas "dinámicas". En este caso, puedes usar la propia instancia del mapa como delegado para implementar propiedades delegadas.
class Site(val map: Map<String, Any?>) {
val name: String by map
val url: String by map
}
fun main(args: Array<String>) {
// 构造函数接受一个映射参数
val site = Site(mapOf(
"name" to "Ejemplo",
"url" to "www.example.com"
))
// 读取映射值
println(site.name)
println(site.url)
}
Resultado de salida:
Ejemplo www.example.com
Si se usa una propiedad `var`, es necesario reemplazar `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 "Ejemplo",
"url" to "www.example.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)
}
Resultado de salida:
Ejemplo www.example.com -------------- Google www.google.com
Not Null
`notNull` es adecuado para situaciones en las que el valor de la propiedad no se puede determinar en la fase de inicialización.
class Foo {
var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)
Ten en cuenta que si la propiedad se accede antes de ser asignada, se lanzará una excepción.
Propiedades delegadas locales
Puedes declarar una variable local como propiedad delegada. Por ejemplo, puedes hacer que una variable local se inicialice perezosamente:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
La variable `memoizedFoo` solo se calculará en el primer acceso. Si `someCondition` falla, la variable no se calculará en absoluto.
Requisitos de la delegación de propiedades
Para propiedades de solo lectura (es decir, propiedades `val`), su delegado debe proporcionar una función llamada `getValue()`. Esta función acepta los siguientes parámetros:
- `thisRef` — debe ser del mismo tipo que el tipo del propietario de la propiedad (para propiedades de extensión, el tipo extendido) o un supertipo de este.
- `property` — debe ser de tipo `KProperty<*>` o un supertipo de este.
Esta función debe devolver el mismo tipo que la propiedad (o un subtipo de este).
Para una propiedad mutable (es decir, una propiedad `var`), además de la función `getValue()`, su delegado también debe proporcionar una función llamada `setValue()`. Esta función acepta los siguientes parámetros:
- `property` — debe ser de tipo `KProperty<*>` o un supertipo de este.
- `new value` — debe ser del mismo tipo que la propiedad o un supertipo de este.
Reglas de traducción
Detrás de la implementación de cada propiedad delegada, el compilador de Kotlin genera una propiedad auxiliar y le delega. Por ejemplo, para la propiedad `prop`, se genera la propiedad oculta `prop$delegate`, y el código del accessor simplemente delega en esta propiedad adicional:
class C {
var prop: Type by MyDelegate()
}
// 这段是由编译器生成的相应代码:
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` hace referencia a la instancia de la clase externa `C`, y `this::prop` es un objeto de reflexión de tipo `KProperty` que describe a `prop` en sí mismo.
Proporcionar un delegado
Al definir el operador `provideDelegate`, se puede extender la lógica de creación del objeto delegado para la implementación de una propiedad. Si el objeto utilizado en el lado derecho de `by` define `provideDelegate` como función miembro o función de extensión, se llamará a esa función para crear la instancia del delegado de la propiedad.
Un caso de uso posible de `provideDelegate` es verificar la consistencia de la propiedad al crearla (no solo en su getter o setter).
Por ejemplo, si deseas verificar el nombre de la propiedad antes de la vinculación, puedes escribir:
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 创建委托
}
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 del mismo tipo que el tipo del propietario de la propiedad (para propiedades de extensión, el tipo extendido) o un supertipo de este.
- `property` — debe ser de tipo `KProperty<*>` o un supertipo de este.
Durante la creación de una instancia de `MyUI`, se llama al método `provideDelegate` para cada propiedad y se ejecuta inmediatamente la validación necesaria.
Sin esta capacidad de interceptar la vinculación entre la propiedad y su delegado, para lograr la misma funcionalidad tendrías que pasar explícitamente el nombre de la propiedad, lo cual no es muy conveniente:
// 检查属性名称而不使用“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)
// 创建委托
}
En el código generado, se llamará al método `provideDelegate` para inicializar la propiedad auxiliar `prop$delegate`. Compara el código generado para la declaración de propiedad `val prop: Type by MyDelegate()` con el código generado anteriormente (cuando no existe el método `provideDelegate`):
class C {
var prop: Type by MyDelegate()
}
// 这段代码是当“provideDelegate”功能可用时
// 由编译器生成的代码:
class C {
// 调用“provideDelegate”来创建额外的“delegate”属性
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}
Ten en cuenta que el método `provideDelegate` solo afecta a la creación de la propiedad auxiliar, no afecta al código generado para el getter o setter.
Otras extensiones