系列文章全部為本人的學習筆記,若有任何不妥之處,隨時歡迎拍磚指正。如果你覺得我的文章對你有用,歡迎關注我,我們一起學習進步!
Kotlin學習筆記(1)- 環境配置
Kotlin學習筆記(2)- 空安全
Kotlin學習筆記(3)- 語法
Kotlin學習筆記(4)- 流程控制
Kotlin學習筆記(5)- 類
Kotlin學習筆記(6)- 屬性
Kotlin學習筆記(7)- 接口
Kotlin學習筆記(8)- 擴展
Kotlin學習筆記(8)- 擴展(續)
Kotlin學習筆記(9)- 數據類
Kotlin學習筆記(10)- 泛型
Kotlin學習筆記(11)- 內部類和嵌套類
Kotlin學習筆記(12)- 委托
Kotlin學習筆記(13)- 函數式編程
Kotlin學習筆記(14)- lambda
一、屬性聲明
一般的說,類是屬性和邏輯的集合。我們用方法封裝和處理邏輯,用變量聲明屬性。所以可以大言不慚的說一句,屬性聲明在類中是必不可少的。在《Kotlin學習筆記(3)- 語法》中我們介紹過,kotlin中的屬性聲明有兩種:var
聲明普通變量,val
聲明只讀變量(暨final
類型)。其實還有幾點需要說明。
-
類中聲明的屬性,一定要初始化,否則會編譯錯誤。除非你對屬性使用了
abstract
進行修飾。var name: String = "" abstract var size : Int
-
屬性聲明中的屬性類型在大部分情況下是可以省略的。這里說的大部分是指我們的最多的使用情況,也就是默認的使用場景。
var name = ""
那什么時候是不能省略的呢?這還要說到另一個問題,那就是屬性的修飾符,和方法類似的,屬性也有多種修飾符。
public // 默認的修飾符。全部可見,在屬性被初始化時, // 如果可以根據屬性的值推斷出屬性類型,則可省略類型 protected // 在本身和子類中可見,如果可以根據屬性的值推斷出屬性類型,則可省略類型 private // 只在本身可見,如果可以根據屬性的值推斷出屬性類型,則可省略類型 abstract // 在自身不初始化,需要子類進行初始化,不能省略類型。
-
類中的屬性,用
.
進行訪問例如上面的name
屬性,我們可以這樣進行讀寫操作var person = Person() person.name = "jck" // 寫 Log.d("log", person.name) // 讀
二、getter和setter
-
默認方法
上面的例子可以看到,我們可以直接調用
person.name
對屬性進行讀寫操作,這種操作我們在java中也見過,是對類中的public
屬性進行操作。而在kotlin中,name
屬性雖然也是public
的,但是意義和java中是完全不同的,這里的讀寫其實是對get
和set
函數的隱式調用,而get
和set
函數是默認實現的,而顯式的寫出來則是這樣// 非源碼,而是根據自己理解寫出的。這里的field下面會說 var name: String = "" get() = field // get set(value) { // set field = value }
也就是說,之前我們說到的
person.name
的讀寫操作,其實是對get
和set
方法的訪問,而并不是像java中的直接對屬性的操作,保證了屬性的閉合性。完整的聲明如下:var <propertyName>: <PropertyType> [= <property_initializer>] [<getter>] [<setter>]
其中initializer, getter 和 setter都是可選的。var是允許有getter 和 setter方法,val不允許有setter方法。如果屬性值的數據類型可以通過編譯器自動推斷,或者在getter和setter方法中并沒有對屬性做特殊處理,這些方法都可以省略。
-
訪問權限
在java中我們可以根據需要決定是否實現屬性的
get
和set
方法,kotlin中自然也有針對針對這種需求的實現。首先屬性都是有get
和set
方法的,當我們不想對外公開某個方法時,可以使用修飾符private
實現,例如var name: String = "" private set
但是這種方式只適用于
set
方法,get
的訪問權限默認是和屬性一致的,下面的使用會報編譯錯誤var name: String = "" private get // 編譯錯誤,get的訪問權限和屬性一致
如果你們有一個需求,要求對某個屬性只可寫,不可讀,那么請用方法
fun
實現吧…… -
自定義getter和setter
上面說了
get
和set
的默認實現,那么就再來說說自定義實現,當然,像上面說過的,var
有set
和get
,val
只有get
// 自定義get var size: Int = 2 get() = if (field > 10) 15 else 0 // 調用 var pf = PropertiesFields() pf.size = 5 Log.d("text", "size : ${pf.size}") pf.size = 20 Log.d("text", "size : ${pf.size}") // 輸出 size : 0 size : 15 // 自定義set var size: Int = 2 set(value) { field = if (value > 10) 15 else 0 } // 調用和輸出同上
上面的代碼很簡單,就是一個
if
表達式 ,就不多解釋了。其實kotlin中的getter
和setter
理解起來很簡單,就像java中的所有屬性都是private
并且實現了getter
和setter
,其他的像權限和自定義問題,都和java中類似。
三、后端變量(Backing Fields)
看大神是這么翻譯Backint Fields的,那我們也這么叫好了。這里就要說到上面提到的field
了,在kotlin的getter
和setter
是不允許本身的局部變量的,因為屬性的調用也是對get
的調用,因此會產生遞歸,造成內存溢出。
var count = 1
var size: Int = 2
set(value) {
Log.e("text", "count : ${count++}")
size = if (value > 10) 15 else 0
}
kotlin為此提供了一種我們要說的后端變量,也就是field
。編譯器會檢查函數體,如果使用到了它,就會生成一個后端變量,否則就不會生成。我們在使用的時候,用field代替屬性本身進行操作。
var size: Int = 2
val isEmpty: Boolean
get() = this.size == 0
這是官方文檔的一個例子,在訪問屬性值isEmpty時,并不會生成后端變量。
這里我有一個疑惑。我們說過類屬性是一定要初始化的,但是我在編譯這個例子的時候確實是沒問題的。我又嘗試著將
isEmpty
改為普通變量,然后就編譯出錯,希望朋友能幫我解惑,謝謝。var size: Int = 2 var isEmpty: Boolean // 這樣就編譯報錯 get() = this.size == 0
四、后端屬性
如果上面的方案都不符合你的需求,那么可以試試“后端屬性”(backing property)的方法,它實際上也是隱含試的對屬性值的初始化聲明,避免了空指針。
private var _table: Map<String, Int>? = null
val table: Map<String, Int>
get() {
if (_table == null)
_table = HashMap() // Type parameters are inferred
return _table ?: throw AssertionError("Set to null by another thread")
}
從各種角度看,這和在Java中定義Bean
屬性的方式一樣。因為訪問私有的屬性的getter
和setter
函數,會被編譯器優化成直接反問其實際字段。
五、編譯器常數值
如果在編譯期間,屬性值就能被確定,該類屬性值使用const 修飾符,將屬性標記為編譯期常數值(compile timeconstants). 這類屬性必須滿足以下所有條件:
- 必須是頂級屬性,或者是一個object的成員
- 值被初始化為 String 類型,或基本類型(primitive type)
- 不存在自定義的取值方法
六、延遲初始化屬性(lateinit)
我們說過,在類內聲明的屬性必須初始化,如果設置非NULL的屬性,應該將此屬性在構造器內進行初始化。假如想在類內聲明一個NULL屬性,在需要時再進行初始化(最典型的就是懶漢式單例模式),與Kotlin的規則是相背的,此時我們可以聲明一個屬性并延遲其初始化,此屬性用lateinit修飾符修飾。
// 延遲初始化聲明
lateinit var late : String
fun initLate(){
late = "I am late"
}
// 先調用方法,再調用屬性
var pf = PropertiesFields()
pf.initLate()
Log.d("text", pf.late)
// 輸出
I am late
需要注意的是,我們在使用的時候,一定要確保屬性是被初始化過的,通常先調用初始化方法,否則會有異常。