Kotlin:Delegated Properties 和 by 關鍵字

Photo by Johan Mouchet on Unsplash
Photo by Johan Mouchet on Unsplash
Kotlin 的 by keyword 提供了 delegated properties 的功能。使用 delegated properties,程式碼可以更簡短且優雅。本文章將介紹如何搭配 by keyword 來使用 delegated properties。

Kotlin 的 by keyword 提供了 delegated properties 的功能。使用 delegated properties,程式碼可以更簡短且優雅。本文章將介紹如何搭配 by keyword 來使用 delegated properties。

內建的 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() 即可。

這兩個方法定義在 ReadOnlyPropertyReadWriteProperty 裡。不過,我們不需要實作這兩個 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 可以幫我們省下這些程式碼,讓程式碼更加地精簡優雅。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More