Kotlin 什麼是幕後欄位?

依然範特稀西發表於2018-09-09

上篇文章我們瞭解了Kotlin中的各種類,從Kotlin的類開始說起,而類中則有屬性和方法,Kotlin 中的類屬性和Java的類成員變數還是有很大區別,同時類屬性也有一些比較難以理解的東西,如:屬性的宣告形式、幕後欄位、幕後屬性等等。本篇文章我們將詳細深入的瞭解這些東西。

1 . 前戲(Kotlin的普通屬性)

在Kotlin中,宣告一個屬性涉及到2個關鍵字,varval

  • 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"
}
複製程式碼

image.png

可以看到,重新給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中,gettersetter 是屬性宣告的一部分,宣告一個屬性預設提供gettersetter ,當然了,如果有需要,你也可以自定義gettersetter。既然要自定義,我們得先理解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中gettersetter 跟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 = ""
}

複製程式碼

上面兩個屬性的宣告是等價的,例子一中的gettersetter 就是預設的gettersetter。其中幕後欄位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 AbstractListSubList原始碼:

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技術雜貨鋪】

相關文章