上篇文章我們瞭解了Kotlin中的各種類,從Kotlin的類開始說起,而類中則有屬性和方法,Kotlin 中的類屬性和Java的類成員變數還是有很大區別,同時類屬性也有一些比較難以理解的東西,如:屬性的宣告形式、幕後欄位、幕後屬性等等。本篇文章我們將詳細深入的瞭解這些東西。
1 . 前戲(Kotlin的普通屬性)
在Kotlin中,宣告一個屬性涉及到2個關鍵字,var
和 val
。
- var 宣告一個可變屬性
- val 宣告一個只讀屬性
通過關鍵字var
宣告一個屬性:
class Person {
var name:String = "Paul"//宣告一個可變屬性,預設值為 Paul
}
複製程式碼
通過var
宣告的屬性是可以改變屬性的值的,如下所示:
fun main(args: Array<String>) {
var person = Person()
// 第一次列印name的值
println("name:${person.name}")
// 重新給name賦值
person.name = "Jake"
//列印name的新值
println("name:${person.name}")
}
複製程式碼
列印結果如下:
name:Paul
name:Jake
複製程式碼
如果把name
屬性換成val
宣告為只讀屬性,在來改變的的值呢?
class Person {
val name:String = "Paul"
}
複製程式碼
可以看到,重新給val
宣告的屬性賦值時,編譯器就會報錯Val cannot be reassigned
,它的值只能是初始化時的值,不能再重新指定。
這是Kotlin的兩種宣告屬性方式,這不是很簡單嗎?一行程式碼。表面很簡單,不過這一行程式碼包含的東西很多,只是沒有顯示出來而已,我們來看一下一個屬性的完整宣告形式:
// 可變屬性
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
// 只讀屬性
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
複製程式碼
瞬間多了很多東西,其初始器(initializer)、getter 和 setter 都是可選的。屬性型別如果可以從初始器 (或者從其 getter 返回值,如下文所示)中推斷出來,也可以省略。也就是我們上面看到的屬性宣告,其實是省略了getter 和 setter 的,已預設提供
var name:String = "Paul" // 使用預設的getter 和setter
複製程式碼
其中初始化的是一個字串,因此可以從初始化起推斷這個屬性就是一個String
型別,所以屬性型別可以省略,變成這樣:
var name = "Paul" // 能推斷出屬性型別,使用預設的getter 和setter
複製程式碼
1. 2 getter & setter
在Kotlin中,getter
、setter
是屬性宣告的一部分,宣告一個屬性預設提供getter
和setter
,當然了,如果有需要,你也可以自定義getter
和setter
。既然要自定義,我們得先理解getter 和 setter 是什麼東西。
在Java 中,外部不能訪問一個類的私有變數,必須提供一個setXXX方法和getXXX方法來訪問,比如Java類Person
,提供了getName()
和setName()
方法供外面方法私有變數name
:
public class Person{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
複製程式碼
在Kotlin中getter
和setter
跟Java 中的getXX 和 setXX方法作用一樣,叫做訪問器。
getter 叫讀訪問器,setter叫寫訪問器。
val
宣告的變數只有讀訪問器getter ,var
宣告的變數讀寫訪問器都有。
Q: 在Kotlin 中,訪問一個屬性的實質是什麼呢?
A: 讀一個屬性,通過.
表示,它的實質就是執行了屬性的getter訪問器,舉個例子:
class Person {
var name:String = "Paul"
}
//測試
fun main(args: Array<String>) {
var person = Person()
// 讀name屬性
val name = person.name
println("列印結果:$name")
}
複製程式碼
列印的結果肯定是:
列印結果:Paul
複製程式碼
然後,我們再來修改getter 的返回值如下:
class Person {
var name:String = "Paul"
get() = "i am getter,name is Jake"
}
//測試
fun main(args: Array<String>) {
var person = Person()
// 讀name屬性
val name = person.name
println("列印結果:$name")
}
複製程式碼
執行結果如下:
列印結果:i am getter,name is Jake
複製程式碼
因此,讀一個屬性的本質是執行了getter, 這跟Java 很像,讀取一個Java類的私有變數,需要通過它提供的get方法。
類似的,在Kotlin中,寫一個屬性的實質就是執行了屬性的寫訪問器setter。 還是這個例子,我們修改一下setter:
class Person {
var name:String = "Paul"
set(value) {
println("執行了寫訪問器,引數為:$value")
}
}
//測試
fun main(args: Array<String>) {
var person = Person()
// 寫name屬性
person.name = "hi,this is new value"
println("列印結果:${person.name}")
}
複製程式碼
執行結果為:
執行了寫訪問器,引數為:hi,this is new value
列印結果:Paul
複製程式碼
可以看到給一個給一個屬性賦值時,確實是執行了寫訪問器setter, 但是為什麼結果還是預設值Paul呢?因為我們重寫了setter,卻沒有給屬性賦值,當然還是預設值。
那麼一個屬性的預設的setter漲什麼樣子呢? 聰明的你可能一下就想到了,這還不簡單,跟Java的 setXXX
方法差不多嘛(傲嬌臉)。一下就寫出來了,如下:
class Person {
//錯誤的演示
var name = ""
set(value) {
this.name = value
}
}
複製程式碼
不好意思,一執行就會報錯,直接StackOverFlow了,記憶體溢位,為什麼呢?轉換為Java程式碼看一下你就明白了,將Person類轉為Java類:
public final class Person {
@NotNull
private String name = "Paul";
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String value) {
this.setName(value);
}
}
複製程式碼
看到沒,方法迴圈呼叫了,setName
中又呼叫了setName
,死迴圈了,直到記憶體溢位,程式崩潰。Kotlin程式碼也一樣,在setter中又給屬性賦值,導致一直執行setter, 陷入死迴圈,直到記憶體溢位崩潰。那麼這個怎麼解決了?這就引入了Kotlin一個重要的東西幕後欄位
2 . 幕後欄位
千呼萬喚始出來,什麼是幕後欄位? 沒有一個確切的定義,在Kotlin中, 如果屬性至少一個訪問器使用預設實現,那麼Kotlin會自動提供幕後欄位,用關鍵字field
表示,幕後欄位主要用於自定義getter和setter中,並且只能在getter 和setter中訪問。
回到上面的自定義setter例子中,怎麼給屬性賦值呢?答案是給幕後欄位field賦值,如下:
class Person {
//錯誤的演示
var name = ""
set(value) {
field = value
}
}
複製程式碼
getter 也一樣,返回了幕後欄位:
// 例子一
class Person {
var name:String = ""
get() = field
set(value) {
field = value
}
}
// 例子二
class Person {
var name:String = ""
}
複製程式碼
上面兩個屬性的宣告是等價的,例子一中的getter
和setter
就是預設的getter
和setter
。其中幕後欄位field
指的就是當前的這個屬性,它不是一個關鍵字,只是在setter和getter的這個兩個特殊作用域中有著特殊的含義,就像一個類中的this
,代表當前這個類。
用幕後欄位,我們可以在getter和setter中做很多事,一般用於讓一個屬性在不同的條件下有不同的值,比如下面這個場景:
場景: 我們可以根據性別的不同,來返回不同的姓名
class Person(var gender:Gender){
var name:String = ""
set(value) {
field = when(gender){
Gender.MALE -> "Jake.$value"
Gender.FEMALE -> "Rose.$value"
}
}
}
enum class Gender{
MALE,
FEMALE
}
fun main(args: Array<String>) {
// 性別MALE
var person = Person(Gender.MALE)
person.name="Love"
println("列印結果:${person.name}")
//性別:FEMALE
var person2 = Person(Gender.FEMALE)
person2.name="Love"
println("列印結果:${person2.name}")
}
複製程式碼
列印結果:
列印結果:Jake.Love
列印結果:Rose.Love
複製程式碼
如上,我們實現了name 屬性通過gender 的值不同而行為不同。幕後欄位大多也用於類似場景。
是不是Kotlin 所有屬性都會有幕後欄位呢?當然不是,需要滿足下面條件之一:
-
使用預設 getter / setter 的屬性,一定有幕後欄位。對於 var 屬性來說,只要 getter / setter 中有一個使用預設實現,就會生成幕後欄位;
-
在自定義 getter / setter 中使用了 field 的屬性
舉一個沒有幕後欄位的例子:
class NoField {
var size = 0
//isEmpty沒有幕後欄位
var isEmpty
get() = size == 0
set(value) {
size *= 2
}
}
複製程式碼
如上,isEmpty
是沒有幕後欄位的,重寫了setter和getter,沒有在其中使用 field
,這或許有點不好理解,我們把它轉換成Java程式碼看一下你可能就明白了,Java 程式碼如下:
public final class NoField {
private int size;
public final int getSize() {
return this.size;
}
public final void setSize(int var1) {
this.size = var1;
}
public final boolean isEmpty() {
return this.size == 0;
}
public final void setEmpty(boolean value) {
this.size *= 2;
}
}
複製程式碼
看到沒,翻譯成Java程式碼,只有一個size變數,isEmpty 翻譯成了 isEmpty()
和setEmpty()
兩個方法。返回值取決於size的值。
有幕後欄位的屬性轉換成Java程式碼一定有一個對應的Java變數
3 . 幕後屬性
理解了幕後欄位,再來看看幕後屬性
有時候有這種需求,我們希望一個屬性:對外表現為只讀,對內表現為可讀可寫,我們將這個屬性成為幕後屬性。 如:
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 型別引數已推斷出
}
return _table ?: throw AssertionError("Set to null by another thread")
}
複製程式碼
將_table
屬性宣告為private
,因此外部是不能訪問的,內部可以訪問,外部訪問通過table
屬性,而table
屬性的值取決於_table
,這裡_table
就是幕後屬性。
幕後屬性這中設計在Kotlin 的的集合Collection中用得非常多,Collection 中有個size
欄位,size
對外是隻讀的,size
的值的改變根據集合的元素的變換而改變,這是在集合內部進行的,這用幕後屬性來實現非常方便。
如Kotlin AbstractList
中SubList
原始碼:
private class SubList<out E>(private val list: AbstractList<E>, private val fromIndex: Int, toIndex: Int) : AbstractList<E>(), RandomAccess {
// 幕後屬性
private var _size: Int = 0
init {
checkRangeIndexes(fromIndex, toIndex, list.size)
this._size = toIndex - fromIndex
}
override fun get(index: Int): E {
checkElementIndex(index, _size)
return list[fromIndex + index]
}
override val size: Int get() = _size
}
複製程式碼
AbstractMap
原始碼中的keys 和 values 也用到了幕後屬性
/**
* Returns a read-only [Set] of all keys in this map.
*
* Accessing this property first time creates a keys view from [entries].
* All subsequent accesses just return the created instance.
*/
override val keys: Set<K>
get() {
if (_keys == null) {
_keys = object : AbstractSet<K>() {
override operator fun contains(element: K): Boolean = containsKey(element)
override operator fun iterator(): Iterator<K> {
val entryIterator = entries.iterator()
return object : Iterator<K> {
override fun hasNext(): Boolean = entryIterator.hasNext()
override fun next(): K = entryIterator.next().key
}
}
override val size: Int get() = this@AbstractMap.size
}
}
return _keys!!
}
@kotlin.jvm.Volatile
private var _keys: Set<K>? = null
複製程式碼
有興趣的可以去翻翻其他原始碼。
4 . 本文總結
本文講了Kotlin 屬性相關的一些知識點,其中需要注意幾個點:
1、屬性的訪問是通過它的訪問器getter和setter, 你可以改變getter和setter 的可見性,比如在setter前新增private
,那麼這個setter就是私有的。
var setterVisibility: String = "abc"
private set // 此 setter 是私有的並且有預設實現
複製程式碼
2、Kotlin 自動提供幕後欄位是要符合條件的(滿足之一):
-
使用預設 getter / setter 的屬性,一定有幕後欄位。對於 var 屬性來說,只要 getter / setter 中有一個使用預設實現,就會生成幕後欄位;
-
在自定義 getter / setter 中使用了 field 的屬性
3、幕後屬性的場景:對外表現為只讀,對內表現為可讀可寫。
以上就是本文全部內容,歡迎討論。
更多Android乾貨文章,關注公眾號 【Android技術雜貨鋪】