Kotlin 什麼是幕後字段?

上篇文章咱們瞭解了Kotlin中的各類類,從Kotlin的類開始提及,而類中則有屬性和方法,Kotlin 中的類屬性和Java的類成員變量仍是有很大區別,同時類屬性也有一些比較難以理解的東西,如:屬性的聲明形式、幕後字段、幕後屬性等等。本篇文章咱們將詳細深刻的瞭解這些東西。java

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

在Kotlin中,聲明一個屬性涉及到2個關鍵字,varvalbash

  • var 聲明一個可變屬性
  • val 聲明一個只讀屬性

經過關鍵字var 聲明一個屬性:markdown

class Person {
    var name:String = "Paul"//聲明一個可變屬性,默認值爲 Paul
}
複製代碼

經過var 聲明的屬性是能夠改變屬性的值的,以下所示:dom

fun main(args: Array<String>) {
   var person = Person()
   // 第一次打印name的值
   println("name:${person.name}")
   // 從新給name賦值
   person.name = "Jake"
   //打印name的新值
   println("name:${person.name}")
}
複製代碼

打印結果以下:jvm

name:Paul
name:Jake
複製代碼

若是把name屬性換成val聲明爲只讀屬性,在來改變的的值呢?ide

class Person {
    val name:String = "Paul"
}
複製代碼

image.png

能夠看到,從新給val聲明的屬性賦值時,編譯器就會報錯Val cannot be reassigned ,它的值只能是初始化時的值,不能再從新指定。oop

這是Kotlin的兩種聲明屬性方式,這不是很簡單嗎?一行代碼。表面很簡單,不過這一行代碼包含的東西不少,只是沒有顯示出來而已,咱們來看一下一個屬性的完整聲明形式:測試

// 可變屬性
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
// 只讀屬性
val <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
複製代碼

瞬間多了不少東西,其初始器(initializer)、getter 和 setter 都是可選的。屬性類型若是能夠從初始器 (或者從其 getter 返回值,以下文所示)中推斷出來,也能夠省略。也就是咱們上面看到的屬性聲明,實際上是省略了getter 和 setter 的,已默認提供this

var name:String = "Paul" // 使用默認的getter 和setter
複製代碼

其中初始化的是一個字符串,所以能夠從初始化起推斷這個屬性就是一個String類型,因此屬性類型能夠省略,變成這樣:spa

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 屬性相關的一些知識點,其中須要注意幾個點:

一、屬性的訪問是經過它的訪問器getter和setter, 你能夠改變getter和setter 的可見性,好比在setter前添加private,那麼這個setter就是私有的。

var setterVisibility: String = "abc"
    private set // 此 setter 是私有的而且有默認實現
複製代碼

二、Kotlin 自動提供幕後字段是要符合條件的(知足之一):

  • 使用默認 getter / setter 的屬性,必定有幕後字段。對於 var 屬性來講,只要 getter / setter 中有一個使用默認實現,就會生成幕後字段;

  • 在自定義 getter / setter 中使用了 field 的屬性

三、幕後屬性的場景:對外表現爲只讀,對內表現爲可讀可寫。

以上就是本文所有內容,歡迎討論。

更多Android乾貨文章,關注公衆號 【Android技術雜貨鋪】

相關文章
相關標籤/搜索