Kotlin 的 by
keyword 提供了 delegated properties 的功能。使用 delegated properties,程式碼可以更簡短且優雅。本文章將介紹如何搭配 by
keyword 來使用 delegated properties。
Table of Contents
內建的 Delegates
Kotlin 內建一些好用的 delegates。在我們開始實作自己的 delegates 之前,先來看看如何使用它們。
Lazy Properties
lazy 是一個很常用的 delegate。它讓我們可以直到第一次使用該變數時才會初始化改變數。
val lazyMessage: String by lazy { println("Initializing") "Hello World!" } fun main() { println("Printing lazyMessage") println(lazyMessage) } // Output: // Printing lazyMessage // Initializing // Hello World!
Observable Properties
observable delegate 讓我們可以實作簡單版的 Observable Pattern。
var observableMessage: String by Delegates.observable("Hello World!") { property, oldValue, newValue -> println("Set ${property.name}, $oldValue -> $newValue") } fun main() { println("Reading observableMessage") observableMessage = "Hello Wayne's Talk!" println("Done") } // Output: // Reading observableMessage // Set observableMessage, Hello World! -> Hello Wayne's Talk! // Done
Votable Properties
votable delegate 是一個變形版的 observable delegate。當該變數在被修改時,它讓你可以決定是否允許該次的修改。
var votableMessage: String by Delegates.vetoable("Hello World!") { property, oldValue, newValue -> if (newValue.startsWith("Hello")) { println("Allow setting ${property.name}, $oldValue -> $newValue") true } else { println("Reject setting ${property.name}, $oldValue -> $newValue") false } } fun main() { votableMessage = "Wayne's Talk!" println(votableMessage) votableMessage = "Hello Wayne's Talk!" println(votableMessage) } // Output: // Reject setting votableMessage, Hello World! -> Wayne's Talk! // Hello World! // Allow setting votableMessage, Hello World! -> Hello Wayne's Talk! // Hello Wayne's Talk!
NotNull Properties
notNull delegate 和 lateinit
keyword 很像,它們可以讓你宣告一個變數而不需要先初始化該變數。但是,lateinit
不支援 primitive types(如 Int 和 Long),此時就你就可以使用 notNull delegate。
// lateinit var amount: Long // Compilation Error var amount: Long by Delegates.notNull() fun main() { try { println("amount is $amount") } catch (e: IllegalStateException) { println("Please set amount before read it: $e") } amount = 100 println("amount is $amount") } // Output: // Please set amount before read it: java.lang.IllegalStateException: Property amount should be initialized before get. // amount is 100
實作一個 Delegate
我們已經看了幾個內建的 delegates,相信讀者已經相當熟悉如何使用 delegates。接下來,我們將介紹如何實作一個 delegate。實作的方法很簡單,我們只要宣告一個 class
,並實作 getValue() 和 setValue() 即可。
這兩個方法定義在 ReadOnlyProperty 和 ReadWriteProperty 裡。不過,我們不需要實作這兩個 interfaces 就可以直接實作 getValue() 和 setValue() 了。如果你的 delegate 是 read-only 的話,那只需要實作 getValue()。
public fun interface ReadOnlyProperty<in T, out V> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public operator fun getValue(thisRef: T, property: KProperty<*>): V } public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public override operator fun getValue(thisRef: T, property: KProperty<*>): V /** * Sets the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @param value the value to set. */ public operator fun setValue(thisRef: T, property: KProperty<*>, value: V) }
以下的程式碼中,我們實作一個類似 lazy delegate 的 delegate。
class MyLazyDelegate<T>(private val initializer: () -> T) { private var value: T? = null operator fun getValue(thisRef: Any?, property: KProperty<*>): T { println("Get ${property.name}") return synchronized(this) { value ?: initializer().also { value = it } } } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { println("old value = ${this.value}, new value = $value") this.value = value } } fun <T> myLazyDelegate(initializer: () -> T) = MyLazyDelegate(initializer) var myTitle: String by myLazyDelegate { println("Initializing") "Hello World!" } fun main() { myTitle = "Hello Wayne's Talk!" println(myTitle) } // Output: // old value = null, new value = Hello Wayne's Talk! // Get myTitle // Hello Wayne's Talk!
Delegate Provider
除了直接宣告變數使用某個 delegate,我們也可以提供一個 provider 來動態地建立一個 delegate。實作方式很簡單,我們需要宣告一個 class
並實作 provideDelegate() 方法。在 provideDelegate() 方法中,你可以實作一些邏輯來決定要建立哪一個 delegate。
在以下的範例中,MyDelegate 和 MyLazyDelegate 都實作 ReadWriteProperty。這是因為 provideDelegate() 必須要回傳一個 ReadWriteProperty。如果,它們不實作 ReadWriteProperty 的話,那麼編譯就會錯誤。
class MyDelegate<T, V>(initializer: () -> V) : ReadWriteProperty<T, V> { private var value: V = initializer() override operator fun getValue(thisRef: T, property: KProperty<*>): V { println("Get ${property.name}") return value } override operator fun setValue(thisRef: T, property: KProperty<*>, value: V) { println("Set ${property.name} to $value") this.value = value } } class MyLazyDelegate<T, V>(private val initializer: () -> V) : ReadWriteProperty<T, V> { private var value: V? = null override operator fun getValue(thisRef: T, property: KProperty<*>): V { println("Get ${property.name}") return synchronized(this) { value ?: initializer().also { value = it } } } override operator fun setValue(thisRef: T, property: KProperty<*>, value: V) { println("old value = ${this.value}, new value = $value") this.value = value } } class MyDelegateLoader<T, V>(private val lazy: Boolean, private val initializer: () -> V) { operator fun provideDelegate( thisRef: T, prop: KProperty<*>, ): ReadWriteProperty<T, V> { return if (lazy) MyLazyDelegate(initializer) else MyDelegate(initializer) } } fun <T, V> myDelegate(lazy: Boolean, initializer: () -> V) = MyDelegateLoader<T, V>(lazy, initializer) var message: String by MyDelegateLoader(true) { println("Initializing") "Wayne's Talk!" } fun main() { message = "Hello Wayne's Talk!" println(message) } // Output: // old value = null, new value = Hello Wayne's Talk! // Get message // Hello Wayne's Talk!
委託給另一個 Property
by
keyword 還可以讓你委託一個 property 給另外一個 property。
var topLevelInt: Int = 0 class ClassWithDelegate(val anotherClassInt: Int) class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) { var delegatedToMember: Int by this::memberInt var delegatedToTopLevel: Int by ::topLevelInt val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt } var MyClass.extDelegated: Int by ::topLevelInt
當你想要重新命名一個 property 又要 backward-compatible 時,這功能會非常好用。
class MyClass { var newName: Int = 0 @Deprecated("Use 'newName' instead", ReplaceWith("newName")) var oldName: Int by this::newName }
儲存 Properties 到 Map 裡
by
keyword 還可以將 properties 對應到 map 裡面。如果你的程式需要大量地解析 JSON 的話,這功能會幫你省下很多程式碼。
class User(map: Map<String, Any>) { val name: String by map val age: Int by map } val user = User( mapOf( "name" to "John Doe", "age" to 25 ) ) fun main() { println(user.name) println(user.age) }
結語
本文中介紹的各種 delegated properties 應用,我們有時會使用到。當是使用 Java 實作時,我們則必須要產生不少程式碼來達到這個功能。當這類的程式碼便多時,會使得程式碼便為凌亂且不容易閱讀。Kotlin 的 by
keyword 可以幫我們省下這些程式碼,讓程式碼更加地精簡優雅。
參考
- Delegated properties, Kotlin Docs.