Kotlin 代理與懶加載機制:使用、lazy 深度解析

Kotlin 代理與懶加載機制:使用、lazy 深度解析

Overview of Content

本文深入探討 Java 與 Kotlin 中的代理、委託的概念,包括 Kotlin 的委託類和委託屬性,使讀者全面了解這一強大的語言特性。

同時,我們將探討 Kotlin 中的懶加載機制,包括延遲化 (lateinit)、by lazy 的單次初始化,並進行 lazy 源碼的分析。透過本文,讀者將獲得對代理模式和懶加載機制的深刻理解,進一步提升在 Java 與 Kotlin 開發中的應用技能。

以下參考,第一行代碼 (第三版), Kotlin 進階實戰,如有引用參考本文章請詳註出處,感謝 😀


Java 代理、Kotlin 委託:概述

● 在 Java 語言中沒有語言層級的委託模式,但相對應的可以透過 OOP 設計的代理模式來實現,而 Java 的代理又分為兩種

A. 靜態代理:快速,編譯期間就完成代理

B. 動態代理:使用反射來動態創建代理對象,消耗資源,但拓展、自由度高

兩者各有優缺點,須依照場合使用(詳情可看 代理模式

● Kotlin 的特別處之一在於「語言層級的代理」,也就是該篇文章要重點說明的,它包括代理類、代理屬性

「代理, Proxy」這個詞也可以替換為「委託, Delegate」,兩者個目的是相同的

Kotlin 委託類

● Kotlin 使用「by 關鍵字」來設定委託類;以下我們來 ProxyShop 代理 RealShop 類,實現與 Java 一樣的代理模式


// 1. 要有共同的接口
interface IShop {
    fun buy(stuffName : String)
}

// 2. 實作類實現接口
open class RealShop : IShop{
    override fun buy(stuffName : String) {
        println("Real Shop buy $stuffName for you.")
    }
}

// 3. 代理類實現接口,並使用 `by` 指定代理
class ProxyShop(realShop: IShop) : IShop by realShop

fun main() {
    val proxy = ProxyShop(RealShop())

    proxy.buy("Unit test of Kotlin")
}

委託類的模式下,可以用來「取代一般的繼承」,以上範例的功能更像是透過建構函數注入(DI by constructor)來完成代理

Kotlin 委託類的另一個特點是,它也可以委託 多個類(或是說委託多個介面),範例如下


interface IShop {
    fun buy(stuffName : String)
}

open class RealShop : IShop{
    override fun buy(stuffName : String) {
        println("Real Shop buy $stuffName for you.")
    }
}

interface IPay {
    fun pay(cost : Int)
}

class LinePay : IPay {
    override fun pay(cost: Int) {
        println("Line Pay: ${cost + 10}")
    }
}

class BankPay : IPay {
    override fun pay(cost: Int) {
        println("Bank Pay: ${cost + 20}")
    }
}

// 委託多個介面
class ProxyShop(realShop: IShop, realPay : IPay) : IShop by realShop, IPay by realPay

fun main() {
    // 指定代理 RealShop, LinePay
    val line = ProxyShop(RealShop(), LinePay())
    line.pay(100)
    line.buy("Unit test of Kotlin")


    // 指定代理 RealShop, BankPay
    val bank = ProxyShop(RealShop(), BankPay())
    bank.pay(100)
    bank.buy("Unit test of Kotlin")
}

Kotlin 委託屬性

● 一般來說我們在取得屬性時,會透過各自屬性的 setter/getter 來取得,在這裡我們也可以用「by 關鍵字」來做委託,之後的都透過委託來達成;

Kotlin 委託屬性格式如下


val/var <property 名稱>: <Type 類型> by <代理 setter/getter 類>

● 覆寫操作符:要委託屬性就必須覆寫 getValue(對應 get), setValue (對應 set) 兩個操作符

● Kotlin 的屬性委託常見的有兩種寫法,範例如下

A. 基礎委託屬性使用


// 委託寄存類
class Delegate {
    
    // 委託 getter
    operator fun getValue(user: User, property: KProperty<*>): Any {
        return "getValue be call -> fieldName: ${property.name}"
    }

    // 委託 setter
    operator fun setValue(user: User, property: KProperty<*>, any: Any) {
        println("setValue be call -> fieldName: ${property.name}")
    }
}

class User {
    // 使用委託屬性

    var name by Delegate()
    var password by Delegate()
}

fun main() {
    val user = User()

    println("${user.name}")
    user.name = "Kyle"

    println("${user.password}")
    user.password = "123456"

}

B. 繼承 ReadWriteProperty<in T, V>:表示委託的屬性是可讀寫的


// 委託寄存類
class DBDelegate<in T, V>(private val field: String, 
                          private val id: Int) :
    // T: 要代理的類型 , V: 屬性的類型
    ReadWriteProperty<T, V> {       

    private val data = arrayOf<MutableMap<String, Any?>>(
        mutableMapOf(
            "id" to 1,
            "age" to 12,
            "name" to "Kyle",
            "password" to "123456"
        ),
        mutableMapOf(
            "id" to 2,
            "age" to 2000,
            "name" to "Alien",
            "password" to "654321"
        )
    )

    override fun getValue(thisRef: T, property: KProperty<*>): V {
        // firstOrNull 是語法糖
        val cache = data.firstOrNull {
            it["id"] == id
        }?.get(field) ?: throw Exception("Cannot not find field: $field")

//        println("Get field of $field value, $cache")

        return cache as V
    }

    override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
//        println("Set field of $field value to $value")

        data.firstOrNull {
            it["id"] == id
        }?.set(field, value)
    }

}

class AccountUser(id: Int) {
    // 要代理的類 AccountUser
    // 返回值 String
    var accountName : String by DBDelegate<AccountUser, String>("name", id)
    var accountAge : Int by DBDelegate<AccountUser, Int>("age", id)
    var accountPassword : String by DBDelegate<AccountUser, String>("password", id)
}

fun main() {
    val account = AccountUser(1)

    println(account.accountName)
    println(account.accountAge)

    println("origin: ${account.accountPassword}")
    account.accountPassword = "Apple123"
    println("changed: ${account.accountPassword}")
}

● 使用 DBDelegate 隔離數據的取得,管理數據,之後要修就不需要修改 AccountUser


lateinit 懶加載

Kotlin 基於 Java NPE 提出空安全的概念,來避免 NPE,除非使用 ? 符號來標示該屬性可為 null

但如果要 自己負責屬性的狀態,則可以使用 lateinit(關鍵字), by lazy(lambda 函數)

延遲化 - lateinit

Kotlin 延遲化就使用 關鍵字 lateinit 來描述該變量即可,這樣 1 一開始就不用賦值為 null,也就不用判空,2 但若是程式錯誤仍然會報錯,也就是說 lateinit 是省略的判空,而自身負責該變量的安全

基礎類型不允許使用 lateinit

先來看看一般使用延遲,就必須使用許多的判空符號,為此必須寫下許多不必要的程式


class MyClass(private var value: Int) {

    fun setValue(v: Int) {
        value = v
    }

    fun getValue() : Int? {
        return value
    }

    fun addValue(v: Int) {
        value +=v
    }
    fun reduceValue(v: Int) {
        value -= v
    }
}

class TestNormal {
    private var m : MyClass? = null     // 為此機制就必需使用許多的判空

    fun setMyClass(myClass: MyClass) {
        this.m = myClass
    }

    fun addTime(v: Int) {
        repeat(v) {
            m?.addValue(1)          // 判空
            m?.addValue(2)          // 判空
            m?.addValue(3)          // 判空
            m?.addValue(4)          // 判空
            m?.addValue(5)          // 判空
        }
    }

    fun reduceTime(v: Int) {
        repeat(v) {
            m?.reduceValue(1)         // 判空
            m?.reduceValue(2)         // 判空
            m?.reduceValue(3)         // 判空
            m?.reduceValue(4)         // 判空
            m?.reduceValue(5)         // 判空
        }
    }

    fun printValue() {
        println("value: ${m?.getValue()}")
    }
}

以下使用 lateinit 來描述 value 變量,就不必不斷判空,但 空指針必須自己負責


class TestLateInit {
    private lateinit var m : MyClass     // lateinit 可以省去 kotlin 判空檢查

    fun setMyClass(myClass: MyClass) {
        this.m = myClass
    }

    fun addTime(v: Int) {
        repeat(v) {
            m.addValue(1)          // 判空
            m.addValue(2)          // 判空
            m.addValue(3)          // 判空
            m.addValue(4)          // 判空
            m.addValue(5)          // 判空
        }
    }

    fun reduceTime(v: Int) {
        repeat(v) {
            m.reduceValue(1)         // 判空
            m.reduceValue(2)         // 判空
            m.reduceValue(3)         // 判空
            m.reduceValue(4)         // 判空
            m.reduceValue(5)         // 判空
        }
    }

    fun printValue() {
        println("value: ${m.getValue()}")
    }
}

fun main(){

    val t = TestLateInit()
    t.setMyClass(MyClass(0))
    t.addTime(3)
    t.printValue()
}

--實作結果--

--不檢查導致的空指針--

● 另外它可以使用一種特定的方式檢查,::<變量>.isInitialized 用來檢查是否已經初始化完成,當然它並不只可以檢查類的初始化,:: 還可以檢查其它判斷


// 使用 isInitialized

private lateinit var m : MyClass 

fun addTime(v: Int) {
    if(::m.isInitialized) {
        repeat(v) {
            m.addValue(1)          // 判空
            m.addValue(2)          // 判空
            m.addValue(3)          // 判空
            m.addValue(4)          // 判空
            m.addValue(5)          // 判空
        }
    } else {
        println("@addTime Haven't Init")
    }
}

--實作結果--

by lazy:單次初始化

● 使用 by lazy{ } 就可以懶加載,並且 該懶加載初始化只會一次!並且只能使用 val 描述

這裡的 by 就是使用了屬性委託機制重寫 getter


val msg : String by lazy {
    println("Msg init ~~")        // 只會初始化一次

    "Hello kotlin by lazy"  // return value
}

fun main() {
    println("first call: $msg")

    println("\nsecond call: $msg")
}

lazy 源碼分析

A. 一般常用的 lazy 源碼是使用 SynchronizedLazyImpl


public actual fun <T> lazy(initializer: () -> T): Lazy<T> 
    = SynchronizedLazyImpl(initializer)

SynchronizedLazyImpl 類實作源碼:其重點是 1. 使用了 Volatile 儲存初始化完後的值,並 2. 使用 synchronized 同步初始化


internal object UNINITIALIZED_VALUE

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            // 同步鎖
            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    // 執行初始化函數
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

Volatile 可以保證多執行序對於屬性操作的可見性

B. 可以指定 mode,每種 mode 都也不同的特性

mode說明
NONE速度快,線程不安全
SYNCHRONIZED第一個執行的 Thread 進程初始化,之後不再初始化
PUBLICATION多個 Thread 可以安全訪問屬性(使用 CAS 機制)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        // 查看 SafePublicationLazyImpl
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

SafePublicationLazyImpl 類實作源碼:其重點是1. 使用了 Volatile 儲存初始化完後的值,並 2. 使用 AtomicReferenceFieldUpdater 執行 CAS 機制


private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    @Volatile private var initializer: (() -> T)? = initializer
    
    // 儲存更新的值
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // this final field is required to enable safe initialization of the constructed instance
    private val final: Any = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val value = _value
            if (value !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return value as T
            }

            val initializerValue = initializer
            // if we see null in initializer here, it means that the value is already set by another thread
            if (initializerValue != null) {
                val newValue = initializerValue()
                // 內部會不停循環比較 
                if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                    initializer = null
                    return newValue
                }
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)

    companion object {
        
        // 使用了 Java atomic 類
        private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
            SafePublicationLazyImpl::class.java,
            Any::class.java,
            "_value"
        )
    }
}

更多的 Kotlin 語言相關文章

在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!

Kotlin 特性、特點

Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用

Kotlin 進階:協程、響應式、異步

Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響