Délégation Kotlin
Patron de délégationLe patron de délégation est une technique fondamentale dans les patrons de conception logicielle. Dans le patron de délégation, deux objets participent au traitement de la même requête. L'objet qui reçoit la requête délègue la requête à un autre objet pour traitement.
Kotlin prend directement en charge le patron de délégation, ce qui le rend plus élégant et concis. Kotlin implémente la délégation au moyen du mot-clé `by`.
Délégation de classe
La délégation de classe signifie que les méthodes définies dans une classe sont en réalité implémentées en appelant les méthodes d'un objet d'une autre classe.
Dans l'exemple suivant, la classe dérivée `Derived` hérite de toutes les méthodes de l'interface `Base` et délègue l'exécution de ces méthodes à un objet entrant de la classe `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() // 输出 10
}
Dans la déclaration de `Derived`, la clause `by` signifie que `b` est stocké à l'intérieur de l'instance d'objet de `Derived`, et le compilateur générera toutes les méthodes héritées de l'interface `Base` et transmettra les appels à `b`.
Délégation de propriété
La délégation de propriété signifie qu'une valeur de propriété d'une classe n'est pas définie directement dans la classe, mais est confiée à une classe déléguée, ce qui permet une gestion unifiée des propriétés de la classe.
Syntaxe de délégation de propriété :
val/var <属性名>: <类型> by <表达式>
- var/val : type de propriété (modifiable/lecture seule)
- Nom de la propriété : le nom de la propriété
- Type : le type de données de la propriété
- Expression : la classe déléguée
L'expression après le mot-clé `by` est le délégué. Les méthodes `get()` (et `set()`) de la propriété seront déléguées aux méthodes `getValue()` et `setValue()` de cet objet. La délégation de propriété n'a pas besoin d'implémenter d'interface, mais elle doit fournir la fonction `getValue()` (pour les propriétés `var`, une fonction `setValue()` est également requise).
Définir une classe déléguée
Cette classe doit contenir les méthodes `getValue()` et `setValue()`. Le paramètre `thisRef` est l'objet de la classe qui effectue la délégation, et `prop` est l'objet de la propriété en cours de délégation.
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 = "Example" // 调用 setValue() 函数
println(e.p)
}
Le résultat de sortie est:
Example@433c675d, 这里委托了 p 属性 Example@433c675d 的 p 属性赋值为 Example Example@433c675d, 这里委托了 p 属性
Délégués standard
La bibliothèque standard de Kotlin dispose de nombreuses méthodes de fabrique intégrées pour implémenter la délégation de propriété.
Propriétés paresseuses
`lazy()` est une fonction qui accepte une expression lambda comme paramètre et renvoie une instance de `Lazy<T>`. L'instance renvoyée peut être utilisée comme délégué pour implémenter des propriétés paresseuses : le premier appel à `get()` exécute l'expression lambda passée à `lazy()` et enregistre le résultat ; les appels suivants à `get()` renvoient simplement le résultat enregistré.
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}
La sortie après exécution est :
computed! Hello Hello
Propriétés observables
`observable` peut être utilisé pour implémenter le modèle observateur.
La fonction `Delegates.observable()` accepte deux paramètres : le premier est la valeur initiale, et le second est le gestionnaire de l'événement de changement de valeur de la propriété.
Après l'affectation de la propriété, le gestionnaire d'événements est exécuté. Il prend trois paramètres : la propriété en cours d'affectation, l'ancienne valeur et la nouvelle valeur :
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 = "第二次赋值"
}
La sortie après exécution est :
Old value: initial value -> New value: first assignment Old value: first assignment -> New value: second assignment
Stockage de propriétés dans une Map
Un cas d'utilisation courant consiste à stocker les valeurs de propriétés dans une map. Cela apparaît souvent dans les applications qui analysent du JSON ou font d'autres choses « dynamiques ». Dans ce cas, vous pouvez utiliser l'instance de map elle-même comme délégué pour implémenter des propriétés déléguées.
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 "Example",
"url" to "www.example.com"
))
// 读取映射值
println(site.name)
println(site.url)
}
La sortie après exécution est :
Example www.example.com
Si vous utilisez une propriété `var`, vous devez remplacer `Map` par `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 "Example",
"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)
}
La sortie après exécution est :
Example www.example.com -------------- Google www.google.com
Non nul
`notNull` convient aux situations où la valeur de la propriété ne peut pas être déterminée pendant la phase d'initialisation.
class Foo {
var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)
Il convient de noter que si la propriété est accédée avant son affectation, une exception sera levée.
Propriétés déléguées locales
Vous pouvez déclarer des variables locales comme propriétés déléguées. Par exemple, vous pouvez faire en sorte qu'une variable locale soit initialisée de manière paresseuse :
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
La variable `memoizedFoo` n’est calculée qu’au premier accès. Si `someCondition` échoue, la variable ne sera pas calculée du tout.
Conditions requises pour la délégation de propriétés
Pour les propriétés en lecture seule (c.-à-d. les propriétés `val`), leur délégué doit fournir une fonction nommée `getValue()`. Cette fonction accepte les paramètres suivants :
- `thisRef` — doit être du même type que le type du propriétaire de la propriété (pour les propriétés d’extension, le type étendu) ou d’un supertype de celui-ci.
- `property` — doit être de type `KProperty<*>` ou d’un supertype de celui-ci.
Cette fonction doit renvoyer le même type que la propriété (ou un sous-type de celui-ci).
Pour une propriété mutable (c.-à-d. une propriété `var`), en plus de la fonction `getValue()`, son délégué doit également fournir une fonction nommée `setValue()`. Cette fonction accepte les paramètres suivants :
- `property` — doit être de type `KProperty<*>` ou d’un supertype de celui-ci.
- `new value` — doit être du même type que la propriété ou d’un supertype de celui-ci.
Règles de traduction
Derrière chaque implémentation de propriété déléguée, le compilateur Kotlin génère une propriété auxiliaire et délègue à celle-ci. Par exemple, pour la propriété `prop`, une propriété masquée `prop$delegate` est générée, et le code de l’accesseur délègue simplement à cette propriété supplémentaire :
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)
}
Le compilateur Kotlin fournit toutes les informations nécessaires sur `prop` dans les paramètres : le premier paramètre `this` fait référence à une instance de la classe externe `C`, et `this::prop` est un objet de réflexion de type `KProperty`, qui décrit `prop` lui-même.
Fournir un délégué
En définissant l'opérateur `provideDelegate`, vous pouvez étendre la logique de création de l'objet auquel l'implémentation de la propriété est déléguée. Si l'objet utilisé du côté droit de `by` définit `provideDelegate` comme fonction membre ou d'extension, cette fonction sera appelée pour créer l'instance de délégué de propriété.
Un cas d'utilisation possible de `provideDelegate` consiste à vérifier la cohérence de la propriété lors de sa création (et pas seulement dans son getter ou setter).
Par exemple, si vous souhaitez vérifier le nom de la propriété avant la liaison, vous pouvez écrire :
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)
}
Les paramètres de `provideDelegate` sont les mêmes que ceux de `getValue` :
- `thisRef` — doit être du même type que le type du propriétaire de la propriété (pour les propriétés d’extension, le type étendu) ou d’un supertype de celui-ci.
- `property` — doit être de type `KProperty<*>` ou d’un supertype de celui-ci.
Lors de la création d'une instance de `MyUI`, la méthode `provideDelegate` est appelée pour chaque propriété, et la validation nécessaire est exécutée immédiatement.
Sans cette capacité d'intercepter la liaison entre la propriété et son délégué, pour obtenir la même fonctionnalité, vous devriez passer explicitement le nom de la propriété, ce qui n'est pas très pratique :
// 检查属性名称而不使用“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)
// 创建委托
}
Dans le code généré, la méthode `provideDelegate` est appelée pour initialiser la propriété auxiliaire `prop$delegate`. Comparez le code généré pour la déclaration de propriété `val prop: Type by MyDelegate()` avec le code généré ci-dessus (lorsque la méthode `provideDelegate` n'existe pas) :
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)
}
Notez que la méthode `provideDelegate` affecte uniquement la création de la propriété auxiliaire et n'affecte pas le code généré pour le getter ou le setter.
Autres extensions