1. 程式人生 > >[譯]Kotlin的屬性委託和惰性初始化是如何工作

[譯]Kotlin的屬性委託和惰性初始化是如何工作

原文連結:https://medium.com/til-kotlin/how-kotlins-delegated-properties-and-lazy-initialization-work-552cbad8be60

Kotlin的屬性委託和惰性初始化是如何工作

訪問屬性在支援面向物件正規化的程式語言中非常常見。Kotlin也提供了許多類似的方法,by lazy進行惰性初始化就是一個很好的例子

在本文中,我們將看看如何使用Kotlin的委託來處理屬性,以及by lazy的惰性初始化,然後深入瞭解它們的工作方式。

Nullable 型別

我認為你們中的許多人可能已經知道nullable

,但讓我們再來看看它。使用Kotlin的Android元件程式碼可以寫成:

class MainActivity : AppCompatActivity() {
    private var helloMessage : String = "Hello"
}

在自己的生命週期初始化和Nullable型別

在上面的例子中,如果在建立物件時可以進行初始化,沒有什麼大問題。但是,如果在特定的初始化過程之後引用它,則不能預先宣告和使用某個值,因為它有自己的生命週期來初始化自己。

我們來看看一些熟悉的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);
} }

可以通過宣告為nullable型別來編寫Kotlin程式碼,如下所示。

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
} }

Non-null型別

上面的程式碼執行良好,但是在使用屬性之前每次檢查它是否為null都有點繁瑣。可以通過使用一個非null型別來忽略它(你相信)它總是有一個值。

class MainActivity: AppCompatActivity () { 
    private var mWelcomeTextView: TextView
    ... 
}

當然,需要使用lateinit說明之後會給這個控制元件賦值

lateinit:我將之後初始化非null屬性

與我們通常談論的延遲初始不同,lateinit允許編譯器識別非null屬性的值未儲存在構造器階段中以便正常編譯。

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應用程式中,大多數控制元件引用在activity的生命週期中保持不變。 換句話說,這意味著很少需要改變分配的引用。

在這一點上,我們可以輕鬆有以下想法:

“如果屬性的值通常儲存在元件的生命週期中,那麼保持該值的只讀型別是否足夠?”

我想是這樣。要做到這一點,乍一看,只需要一點努力就可以用val替換var

只讀屬性的非null的困境

但是,當宣告只讀屬性時,我們面臨的問題是無法定義執行初始化的位置。

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
    }
}

現在讓我們試著解決最後的問題:

“如何使用之後分配只讀屬性”

惰性初始化

在Kotlin中,對只讀屬性執行延遲初始化時by lazy可能非常有用。

by lazy { ... }在屬性第一次使用時執行初始化,而不是宣告時。

class MainActivity : AppCompatActivity() {
    private val messageView : TextView by lazy {
        // runs on first access of messageView
        findViewById(R.id.message_view) as TextView
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    fun onSayHello() {
        // Initialization would be run at here!!
        messageView.text = "Hello"
    }
}

現在,我們可以宣告一個只讀屬性,而不用擔心messageView的初始化位置。讓我們看看by lazy如何工作的。

代理屬性 101

代表團的字面意思是委派。這意味著委託人可以替代原來的訪問者執行一些操作。

委託屬性代理了屬性的getter/setter,它允許代理物件在讀寫值時執行一些內部操作。

Kotlin支援將介面(Class Delegation)或訪問器(Delegated Properties)的實現委託給另一個物件。更多細節將在另一篇文章中介紹。:)

代表團是具有歷史背景的事物。:)(來源:[維基百科公用)](https://cdn-images-1.medium.com/max/1600/1*EThddmBwZW8EZT-JucFHcg.jpeg)

可以緊跟by <delegate>格式宣告一個代理屬性:

val / var \

`by lazy`如何工作的

現在讓我們再次訪問該屬性的程式碼。
只是代理屬性

我們可以認為by lazy將一個屬性作為帶lazy委託的委託屬性。

因此,lazy是如何工作的?讓我們總結一下Kotlin標準庫引用的lazy(),如下所示:

  1. lazy()返回Lazy<T>示例,該示例儲存了lambda初始化器。
  2. 第一次呼叫getter執行傳遞給lazy()的lambda表示式,並儲存表示式的結果。
  3. 之後,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 <b>String getMyName()</b> {
        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$delegategetValue()方法返回Lazy例項的值。

Lazy實現

lazy返回Lazy<T>物件,該物件根據執行緒執行模型(LazyThreadSafetyMode)以些許不同的方式呼叫lambda方法(初始化器)執行初始化操作。

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

他們都負責呼叫給定的lambda塊進行延遲初始化。

SYNCHRONIZED → SynchronizedLazyImpl

PUBLICATION → SafePublicationLazyImpl

  • 它可以同時從多個執行緒中呼叫,並且初始化可以在全部或部分執行緒上同時完成。
  • 然而,如果值已經被其他執行緒初始化,它將直接放而不執行初始化操作。

NONE → UnsafeLazyImpl
* 只需在第一次訪問時初始化,或者返回儲存的值。
* 沒有考慮多執行緒,所以它是不安全的。

Lazy實現的預設行為

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl 通過以下流程執行惰性初始化。讓我們看看前面的例子。
簡單例子總是好的!:)

  1. 屬性的initializer儲存傳入的初始化lambda表示式。

  2. 通過_value屬性儲存值。這個屬性的 初始值是UNINITIALIZED_VALUE

  3. 如果讀操作時,_value的值是UNINITIALIZED_VALUE,則執行initializer表示式。
  4. 如果_valueUNINITIALIZED_VALUE,讀操作將返回_value,因為初始化已經完成了。

SynchronizedLazyImpl

如果不指定模式,惰性實現則使用SynchronizedLazyImpl,該模式只執行一次初始化操作。讓我們看看它的實現程式碼

private object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl(
        initializer: () -> T,
        lock: Any? = null
) : Lazy,
    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()執行初始化塊。
  • 由於其他執行緒可能已經完成了初始化操作,因此它執行了雙重檢查。如果初始化已經完成,它直接返回儲存的值。
  • 如果沒有初始化,他講執行lambda表示式並存儲返回值。接著initializer賦值為null,因為初始化完成後不再需要它了。

Kotlin的代理屬性

當然,惰性初始化有時會導致問題發生,或者在異常情況下繞過控制流並生成正常值,從而使除錯變得困難。

但是,如果您對這些情況非常小心,那麼Kotlin的惰性初始化可以讓我們免於擔心執行緒安全和效能問題。

我們還確認了惰性初始化是運算子和惰性函式的結果。還有更多的代理方式,比如ObservablenotNull。如果有必要,你也可以實現有趣的委託屬性。請享用吧!