[譯]Kotlin的屬性委託和惰性初始化是如何工作
Kotlin的屬性委託和惰性初始化是如何工作
訪問屬性在支援面向物件正規化的程式語言中非常常見。Kotlin也提供了許多類似的方法,by lazy
進行惰性初始化就是一個很好的例子
在本文中,我們將看看如何使用Kotlin的委託來處理屬性,以及by lazy
的惰性初始化,然後深入瞭解它們的工作方式。
Nullable 型別
我認為你們中的許多人可能已經知道nullable
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()
,如下所示:
lazy()
返回Lazy<T>
示例,該示例儲存了lambda初始化器。- 第一次呼叫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 <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$delegate
的getValue()
方法返回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
- 僅在第一個呼叫的執行緒執行初始化。
- 其他執行緒接著獲得快取值。
- 預設模式(LazyThreadSafetyMode.SYNCHRONIZED)
PUBLICATION → SafePublicationLazyImpl
- 它可以同時從多個執行緒中呼叫,並且初始化可以在全部或部分執行緒上同時完成。
- 然而,如果值已經被其他執行緒初始化,它將直接放而不執行初始化操作。
NONE → UnsafeLazyImpl
* 只需在第一次訪問時初始化,或者返回儲存的值。
* 沒有考慮多執行緒,所以它是不安全的。
Lazy實現的預設行為
SynchronizedLazyImpl
、 SafePublicationLazyImpl
和UnsafeLazyImpl
通過以下流程執行惰性初始化。讓我們看看前面的例子。
屬性的
initializer
儲存傳入的初始化lambda表示式。通過
_value
屬性儲存值。這個屬性的 初始值是UNINITIALIZED_VALUE
。- 如果讀操作時,
_value
的值是UNINITIALIZED_VALUE
,則執行initializer
表示式。
- 如果
_value
值不是UNINITIALIZED_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的惰性初始化可以讓我們免於擔心執行緒安全和效能問題。
我們還確認了惰性初始化是運算子和惰性函式的結果。還有更多的代理方式,比如Observable
和notNull
。如果有必要,你也可以實現有趣的委託屬性。請享用吧!