Kotlin常用的by lazy你真的瞭解嗎
highlight: a11y-dark
「這是我參與2022首次更文挑戰的第10天,活動詳情檢視:2022首次更文挑戰」
前言
在使用Kotlin語言進行開發時,我相信很多開發者都信手拈來地使用by或者by lazy來簡化你的屬性初始化,但是by lazy涉及的知識點真的瞭解嗎 假如讓你實現這個功能,你會如何設計。
正文
話不多說,我們從簡單的屬性委託by來說起。
委託屬性
什麼是委託屬性呢,比較官方的說法就是假如你想實現一個比較複雜的屬性,它們處理起來比把值儲存在支援欄位中更復雜,但是卻不想在每個訪問器都重複這樣的邏輯,於是把獲取這個屬性例項的工作交給了一個輔助物件,這個輔助物件就是委託。
比如可以把這個屬性的值儲存在資料庫中,一個Map中等,而不是直接呼叫其訪問器。
看完這個委託屬性的定義,假如你不熟悉Kotlin也可以理解,就是我這個類的例項由另一個輔助類物件來提供,但是這時你可能會疑惑,上面定義中說的支援欄位和訪問器是什麼呢,這裡順便給不熟悉Kotlin的同學普及一波。
Java的屬性
當你定義一個Java類時,在定義欄位時並不是所有欄位都是屬性,比如下面程式碼:
```js //Java類 public class Phone {
//3個欄位
private String name;
private int price;
private int color;
//name欄位訪問器
private String getName() {
return name;
}
private void setName(String name){
this.name = name;
}
//price欄位訪問器
private int getPrice() {
return price;
}
private void setPrice(int price){
this.price = price;
}
} ``` 上面我在Phone類中定義了3個欄位,但是隻有name和price是Phone的屬性,因為這2個欄位有對應的get和set,也只有符合有getter和setter的欄位才叫做屬性。
這也能看出Java類的屬性值是儲存在欄位中的,當然你也可以定義setXX函式和getXX函式,既然XX屬性沒有地方儲存,XX也是類的屬性。
Kotlin的屬性
而對於Kotlin的類來說,屬性定義就非常簡單了,比如下面類:
js
class People(){
val name: String? = null
var age: Int? = null
}
在Kotlin的類中只要使用val/var定義的欄位,它就是類的屬性,然後會自帶getter和setter方法(val屬性相當於Java的final變數,是沒有set方法的),比如下面:
js
val people = People()
//呼叫name屬性的getter方法
people.name
//呼叫age屬性的setter方法
people.age = 12
這時就有了疑問,為什麼上面程式碼定義name時我在後面給他賦值了即null值,和Java一樣不賦值可以嗎 還有個疑問就是在Java中是把屬性的值儲存在欄位中,那Kotlin呢,比如name這個屬性的值就儲存給它自己嗎
帶著問題,我們繼續分析。
Kotlin屬性訪問器
前面我們可知Java中的屬性是儲存在欄位中,或者不要欄位,其實Kotlin也可以,這個就是給屬性定義自定義setter方法和getter方法,如下程式碼:
js
class People(){
val name: String? = null
var age: Int = 0
//定義了isAbove18這個屬性
var isAbove18: Boolean = false
get() = age > 18
}
比如這裡自定義了get訪問器,當再訪問這個屬性時,便會呼叫其get方法,然後進行返回值。
Kotlin屬性支援欄位field
這時一想那Kotlin的屬性值儲存在哪裡呢,Kotlin會使用一個field的支援欄位來儲存屬性。如下程式碼:
```js class People{ val name: String? = null var age: Int = 0 //返回field的值 get() = field //設定field的值 set(value){ Log.i("People", "舊值是$field 新值是$value ") field = value }
var isAbove18: Boolean = false
get() = age > 18
} ``` 可以發現每個屬性都會有個支援欄位field來儲存屬性的值。
好了,為了介紹為什麼Kotlin要有委託屬性這個機制,假如我在一個類中,需要定義一個屬性,這時獲取屬性的值如果使用get方法來獲取,會在多個類都要寫一遍,十分不符合程式碼設計,所以委託屬性至關重要。
委託屬性的實現
在前面說委託屬性的概念時就說了,這個屬性的值需要由一個新類來代理處理,這就是委託屬性,那我們也可以大概猜出委託屬性的底層邏輯,大致如下面程式碼:
js
class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//email屬性進行委託,把它委託給ProduceEmail類
var email: String by ProduceEmail()
}
假如People的email屬性需要委託,上面程式碼編譯器會編譯成如下:
js
class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委託類的例項
private val productEmail = ProduceEmail()
//委託屬性
var email: String
//訪問器從委託類例項獲取值
get() = productEmail.getValue()
//設定值把值設定進委託類例項
set(value) = productEmail.setValue(value)
}
當然上面程式碼是編譯不過的,只是說一下委託的實現大致原理。那假如想使ProduceEmail類真的具有這個功能,需要如何實現呢。
by約定
其實我們經常使用 by 關鍵字它是一種約定,是對啥的約定呢 是對委託類的方法的約定,關於啥是約定,一句話說明白就是簡化函式呼叫,具體可以檢視我之前的文章:
# Kotlin invoke約定,讓Kotlin程式碼更簡潔
那這裡的by約定簡化了啥函式呼叫呢 其實也就是屬性的get方法和set方法,當然委託類需要定義相應的函式,也就是下面這2個函式:
```js //by約定能正常使用的方法 class ProduceEmail(){
private val emails = arrayListOf("[email protected]")
//對應於被委託屬性的get函式
operator fun getValue(people: People, property: KProperty<*>): String {
Log.i("zyh", "getValue: 操作的屬性名是 ${property.name}")
return emails.last()
}
//對於被委託屬性的get函式
operator fun setValue(people: People, property: KProperty<*>, s: String) {
emails.add(s)
}
} ``` 定義完上面委託類,便可以進行委託屬性了:
js
class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委託屬性
var email: String by ProduceEmail()
}
然後看一下呼叫地方:
js
val people = People()
Log.i("zyh", "onCreate: ${people.email}")
people.email = "[email protected]"
Log.i("zyh", "onCreate: ${people.email}")
列印如下:
會發現每次呼叫email屬性的訪問器方法時,都會呼叫委託類的方法。
關於委託類中的方法,當你使用by關鍵字時,IDE會自動提醒,提醒如下:
比如getValue方法中的引數,第一個就是接收者了,你這個要委託的屬性是哪個類的,第二個就是屬性了,關於KProperty不熟悉的同學可以檢視文章:
# Kotlin反射全解析3 -- 大展身手的KProperty
它就代表這屬性,可以呼叫其中的一些方法來獲取屬性的資訊。
而且方法必須使用operator關鍵字修飾,這是過載操作符的必須步驟,想使用約定,就必須這樣幹。
by lazy的實現
由前面明白了by的原理,我們接著來看一下我們經常使用的by lazy是個啥,直接看程式碼:
js
//這裡使用by lazy惰性初始化一個例項
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }
比如上面程式碼,使用惰性初始化初始了一個例項,我們來看一下這個by的實現:
js
//by程式碼
@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
哦,會發現它是Lazy類的一個擴充套件函式,按照前面我們對by的理解,它就是把被委託的屬性的get函式和getValue進行配對,所以可以想象在Lazy< T >類中,這個value便是返回的值,我們來看一下:
```js
//惰性初始化類
public interface Lazy
//懶載入的值,一旦被賦值,將不會被改變
public val value: T
//表示是否已經初始化
public fun isInitialized(): Boolean
} ``` 到這裡我們注意一下 by lazy的lazy,這個就是一個高階函式,來建立Lazy例項的,lazy原始碼:
js
//lazy原始碼
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
這裡會發現第一個引數便是執行緒同步的模式,第二個引數是初始化器,我們就直接看一下最常見的SYNCHRONIZED的模式程式碼:
```js
//執行緒安全模式下的單例
private class SynchronizedLazyImpl
override val value: T
//見分析1
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)
} ``` 分析1:這個單例實現是不是有點眼熟,沒錯它就是雙重校驗鎖實現的單例,假如你對雙重校驗鎖的實現單例方式還不是很明白可以檢視文章:
這裡實現懶載入單例的模式就是雙重校驗鎖,2次判空以及volatile關鍵字都是有作用的,這裡不再贅述。
總結
先搞明白by的原理,再理解by lazy就非常好理解了,雖然這些關鍵字我們經常使用,不過看一下其原始碼實現還是很舒爽的,尤其是Kotlin的高階函式的一些SDK寫法還是很值的學習。
- Kotlin常用的by lazy你真的瞭解嗎
- 協程(23) | Flow原理解析
- 協程(22) | Channel原理解析
- View工作原理 | 理解MeasureSpec和LayoutParams
- LiveData原始碼分析1 -- 概述和簡單使用
- LiveData原始碼分析2 -- 原理分析
- framework | 一文搞定JNI原理
- Android View | Canvas詳解
- Hilt入門 看這一篇就夠了!!
- Android的Window詳解
- Android的Drawable詳解
- 萬字長文解析側滑選單的倆種實現原理
- Kotlin Flow? 真香!
- Java併發程式設計 | 訊號量
- Java併發程式設計 | 區域性變數為什麼是執行緒安全的
- 協程粉碎計劃 | 執行緒排程原理解析
- 協程粉碎計劃 | launch原理解析
- 協程粉碎計劃 | Continuation以及實現掛起函式
- 協程粉碎計劃 | 掛起函式原理解析
- 協程粉碎計劃 | 異常處理