[譯]帶你揭開Kotlin中屬性代理和懶加載語法糖衣

翻譯說明:html

原標題: How Kotlin’s delegated properties and lazy-initialization workjava

原文地址: medium.com/til-kotlin/…git

原文做者: Chang W. Dohgithub

在支持面向對象範式的編程語言中,相信你們對訪問屬性應該很是熟悉了吧。Kotlin就提供了不少這樣的方法,經過by lazy實現屬性的懶加載就是一個很好的例子。算法

在這篇文章中,咱們將一塊兒去看看如何使用Kotlin中的委託屬性以及by lazy的懶加載而後深刻了解它們內部的工做原理,一步步揭開它們語法糖衣。編程

可空類型

我認爲大家中不少人對nullable已經瞭然於胸,可是讓咱們再來看看它。咱們使用Kotlin來開發Android時你可能會像以下這樣寫:設計模式

class MainActivity : AppCompatActivity() {
    private var helloMessage : String = "Hello"
}
複製代碼

可空類型在本身生命週期內初始化

在上述例子中,在對象建立的時候就初始化,這也沒什麼大的問題。然而,若是在特定的初始化過程以後引用它,則不能提早聲明和使用值,由於它有本身的生命週期來初始化自身。數組

讓咱們一塊兒來看下一些熟悉Java代碼緩存

public class MainActivity extends AppCompatActivity {
    private TextView mWelcomeTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWelcomeTextView = (TextView) findViewById(R.id.msgView);
    }
}
複製代碼

你可使用Kotlin來實現上述代碼,經過將上述mWelcomeTextView聲明成可空類型就能夠了.安全

class MainActivity : AppCompatActivity() {
    private var mWelcomeTextView: TextView? = null//聲明成可空類型
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mWelcomeTextView = findViewById(R.id.msgView) as TextView
    }
}
複製代碼

非空類型

上面那個例子代碼運行良好,可是在代碼中使用屬性的以前每次都須要檢查它是否爲null就顯得難受了。這一點你徹底可使用非空類型實現它。

class MainActivity: AppCompatActivity () { 
    private var mWelcomeTextView: TextView
    ... 
}
複製代碼

固然上述代碼,你須要使用lateinit來告訴編譯器,你將稍後爲組件mWelcomeTextView初始化值。

lateinit: 我稍後會初始化非空類型的屬性

與咱們一般討論的延遲初始化(lazy initialization) 不一樣的是,lateinit容許編譯器識別非空類型屬性的值不存儲在構造函數階段以至於能夠正常編譯。

class MainActivity : AppCompatActivity() {
    private lateinit var mWelcomeTextView: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mWelcomeTextView = findViewById(R.id.msgView) as TextView
    }
}
複製代碼

若是你想要了解更多,請查看這裏

只讀屬性

一般,若是組件的字段不是基本數據類型或者內置類型,則能夠發現引用是保留在組件整個生命週期中的。

例如,在Android應用程序中,大多數的組件引用是在Acitivity生命週期中保持不變的。換句話說,這就意味着你不多須要更改組件的引用。

基於這一點,咱們能夠很容易想到如下這點:

「若是屬性的值一般保留在組件的生命週期中,那麼只讀類型的屬性是否足以保持該值?」

我認爲能夠的,要作到這一點,乍看一眼只需將var改成val一點改動就能夠了。

非空只讀屬性的問題

可是,當咱們聲明只讀屬性時,咱們面臨的問題是沒法定義執行初始化的位置

class MainActivity : AppCompatActivity() {
    private val mWelcomeTextView: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Where do I move the initialization code????? 我應該把這個初始化的代碼移到哪呢???
        // mWelcomeTextView = findViewById(R.id.msgView) as TextView
    }
}
複製代碼

如今讓咱們嘗試解決最後一個問題:

「咱們在哪初始化只讀屬性呢」

懶加載(lazy Initialization)

當實如今Kotlin中執行延遲初始化的只讀屬性是,by lazy也許就特別有用了。

by lazy{...}執行初始化程序,其中首先使用的是定義的屬性,而不是它的聲明。

class MainActivity : AppCompatActivity() {
    private val messageView : TextView by lazy {
        // 下面這段代碼將在第一次訪問messageView時執行
        findViewById(R.id.message_view) as TextView
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    fun onSayHello() {
        // 真正初始化將會在這執行!!
        messageView.text = "Hello"
    }
}
複製代碼

如今,咱們能夠聲明一個只讀屬性,而沒必要擔憂messageView的初始化點的問題。讓咱們看看懶加載背後原理是怎麼樣的。

屬性委託(代理)

Delegation的意思就是委託。它意味着經過委託者能夠執行某些操做,而不是直接經過原始訪問者執行操做。

屬性委託是委託屬性的getter/setter方法,它容許委託對象在讀取和寫入值時插入執行一些中間操做。

Kotlin將支持接口(類委託)或訪問器(委託屬性)的實現委託給另外一個對象。

Delegation is a something with a historic background. :) (Source: Wikipedia commons )

你能夠經過by <delegate>形式來聲明一個委託屬性

val / var <property name>: <Type> by <delegate>
複製代碼

屬性的委託能夠像以下方式定義:

class Delegate {
    operator fun getValue( thisRef: Any?, property: KProperty<*> ): String {
        // return value
    }
    operator fun setValue( thisRef: Any?, property: KProperty<*>, value: String ) {
        // assign
    }
}
複製代碼

對值的全部讀取操做都會委託調用getValue()方法,同理,對值的全部寫操做都會委託調用setValue()方法。

by lazy的工做原理

如今讓咱們再次從新研究下上述例子中屬性的代碼。

它實際上就是一個屬性委託!

咱們能夠把by lazy修飾的屬性理解爲是具備lazy委託的委託屬性。

因此,lazy是如何工做的呢? 讓咱們一塊兒在Kotlin標準庫參考中總結lazy()方法,以下所示:

  • 一、lazy() 返回的是一個存儲在lambda初始化器中的Lazy<T>類型實例。
  • 二、getter的第一次調用執行傳遞給lazy()的lambda並存儲其結果。
  • 三、後面的話,getter調用只返回存儲中的值。

簡單地說,lazy建立一個實例,在第一次訪問屬性值時執行初始化,存儲結果並返回存儲的值。

帶有lazy()的委託屬性

讓咱們編寫一個簡單的Kotlin代碼來檢查lazy的實現。

class Demo {
    val myName: String by lazy { "John" }
}
複製代碼

若是你將其反編譯爲Java代碼,則能夠看到如下代碼:

public final class Demo {
    @NotNull
    private final Lazy myName$delegate;
    
    // $FF: synthetic field
    static final KProperty[] $$delegatedProperties = ...
    @NotNull
    public final String getMyName() {
        Lazy var1 = this.myName$delegate;
        KProperty var3 = $$delegatedProperties[0];
        return (String)var1.getValue();
    }
    public Demo() {
        this.myName$delegate =
            LazyKt.lazy((Function0)null.INSTANCE);
    }
}
複製代碼
  • $delegate後綴被拼接到字段名稱後面: myName$delegate
  • 注意myName$delegate的類型是Lazy類型不是String類型
  • 在構造器中,LazyKt.lazy()函數返回值賦值給了myName$delegate
  • LazyKt.lazy()方法負責執行指定的初始化塊

調用getMyName()方法實際過程是將經過調用myName$delegateLazy 實例中的getValue()方法並返回相應的值。

Lazy的具體實現

lazy()方法返回的是一個Lazy<T>類型的對象,該對象處理lambda函數(初始化程序塊),根據線程執行模式(LazyThreadSafetyMode)以稍微幾種不一樣的方式執行初始化。

@kotlin.jvm.JvmVersion
public fun <T> lazy( mode: LazyThreadSafetyMode, initializer: () -> T
): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED ->
            SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION ->
            SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE ->
            UnsafeLazyImpl(initializer)
    }
複製代碼

全部這些都負責調用給定的lambda塊進行延遲初始化

SYNCHRONIZEDSynchronizedLazyImpl

  • 初始化操做僅僅在首先調用的第一個線程上執行
  • 而後,其餘線程將引用緩存後的值。
  • 默認模式就是(LazyThreadSafetyMode.SYNCHRONIZED)

PUBLICATIONSafePublicationLazyImpl

  • 它能夠同時在多個線程中調用,而且能夠在所有或部分線程上同時進行初始化。
  • 可是,若是某個值已由另外一個線程初始化,則將返回該值而不執行初始化。

NONEUnsafeLazyImpl

  • 只需在第一次訪問時初始化它,或返回存儲的值。
  • 不考慮多線程,因此它不是線程安全的。

Lazy實現的默認行爲

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl經過如下過程執行延遲初始化。咱們來看看前面的例子。

  • 一、將傳入的初始化lambda塊存儲在屬性的 initializer

  • 二、經過屬性_value來存儲值。此屬性最開始初始值爲UNINITIALIZED_VALUE

  • 三、在執行讀取操做(屬性get訪問器)時,若是_value的值是最開始初始值UNINITIALIZED_VALUE,那麼就會去執行 initializer初始化器

  • 四、在執行讀取操做(屬性get訪問器)時,若是_value的值不是等於UNINITIALIZED_VALUE, 那就說明初始化操做已經執行完成了。

SynchronizedLazyImpl

若是你沒有明確指定具體模式,延遲具體實現就是SynchronizedLazyImpl,它默認只執行一次初始化。咱們來看看它的實現代碼。

private 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)
}
複製代碼

這看起來有點複雜。但它只是多線程的通常實現。

  • 使用synchronized()同步塊執行初始化塊
  • 因爲初始化可能已經由另外一個線程完成,它會進行雙重鎖檢測(DCL),若是已經完成了初始化,則返回存儲的值。
  • 若是它未初始化,它將執行lambda表達式並存儲返回值。那麼隨後這個initializer將會置爲null,由於初始化完成後就再也不須要它了。

Kotlin的委託屬性將會讓你快樂

固然,延遲初始化有時會致使問題發生或經過繞過控制流並在異常狀況下生成正常值來使調試變得困難。

可是,若是你對這些狀況很是謹慎,那麼Kotlin的延遲初始化可使咱們更加自由地避免對線程安全性和性能的擔心。

咱們還研究了延遲初始化是運算符 bylazy函數的共同做用的結果。還有更多的委託,如ObservablenotNull。若有必要,你還能夠實現有趣的委託屬性。

讀者有話說

閒聊幾句,有一些小夥伴在私下問我對近期2019 Google IO 大會宣佈Kotlin爲Android開發首選語言,即Kotlin-First.這件事怎麼看? 其實我的對於這個比較日常心,以爲不是什麼特別新奇的事,說真的Kotlin自身有能力和優點充當這麼個角色。若是你還在猶豫要不要學Kotlin這門語言的時候,那你就去看看Google IO使用Android代碼例子幾乎全是Kotlin編寫,並且後面不少官方的框架和庫都是Kotlin編寫。我們仍是好好紮實把Kotlin中每一個細節點吃透吧,那麼你就會真正領會到Kotlin這門語言的設計哲學了。

說下爲何要翻譯這篇文章?

這篇文章能夠說把by lazy 屬性懶加載的從使用場景到原理剖析都闡述的很是清楚,並以圖表形式把by lazy調用過程邏輯呈現得簡單易懂。

你是否在Kotlin代碼使用過by lazy呢?那你知道何時該使用lateinit,何時使用by lazy嗎?實際上這篇文章已經給出答案了,這裏簡單大體說下: by lazy正如文章中說的那樣用於非空只讀屬性,須要延遲加載狀況,而lateinit通常用於非空可變屬性,須要延遲加載狀況。後續會有相關文章詳細闡述它們區別以及使用場景。

其實這篇文章還涉及到一個點,那就是 by lazy中的SynchronizedLazyImpl,實際上經過反編譯後代碼可知它是DCL(double check lock),因此能夠利用Companion Object + by lazy能夠實現Kotlin中的DCL單例模式。具體可參考我以前那篇文章當Kotlin完美邂逅設計模式之單例模式(一)

Kotlin系列文章,歡迎查看:

Kotlin邂逅設計模式系列:

數據結構與算法系列:

翻譯系列:

原創系列:

Effective Kotlin翻譯系列

實戰系列:

相關文章
相關標籤/搜索