Kotlin——中級篇(二): 屬性與欄位詳解

Jetictors發表於2018-07-11

Kotlin——中級篇(二): 屬性與欄位詳解

在前面的章節中,詳細的為大家講解到了Kotlin中對類的類的定義、使用、初始化、初始化、類繼承等內容,但是在一個類中,幾乎上是不可能不出現屬性與欄位(field)的,這一篇文章就為大家奉上Kotlin屬性與欄位的定義、使用及高階操作等。如果您目前對Kotlin中的類沒有一個認知的話,請參見Kotlin——中級篇(一):類(class)詳解.

目錄

Kotlin——中級篇(二): 屬性與欄位詳解

一、屬性的基礎使用

一個類中是可以存在屬性的,一個屬性可以用val或者var修飾。

  • val修飾符修飾的屬性是隻讀的,即不能被修改,只可使用
  • val修飾符修飾的屬性是可讀寫的,即能用能改

例:

class Mime{
    val id : String = "123"
    var name : String? = "kotlin"
    var age : Int? = 22
    var sex : String? = "男"
    var weight : Float = 120.3f

    private var test : String = ""
        get() = "123"
        set(value){field = value}
}

fun main(args: Array<String>) {
    val mime = Mime()
    println("id = ${mime.id} \t name = ${mime.name} \t age = ${mime.age}
    \t sex = ${mime.sex} \t weight = ${mime.weight}")
}
複製程式碼

輸出結果為:

id = 123 	 name = kotlin 	     age = 22       sex =男 	 weight = 120.3
複製程式碼

注意:

`val`關鍵字為只讀,`var`為可讀寫。這裡舉例說明:
複製程式碼

例: 還是上面的例子

// id是隻讀的,其他的屬性是可讀寫的

mime.id = "123456"   // 編輯器直接會報錯
mime.name = "kotlin2"  // 可以,因為是var修飾的
...
複製程式碼

二、Getter()與Setter()

  • getter()對應Java中的get()函式setter()對應Java中的set()函式。不過注意的是,不存在Getter()與Setter()的,這只是Kotlin中的叫法而已,真是的寫法,還是用get()、set()。可以看下面的例子。
  • Kotlin中,普通的類中一般是不提供getter()setter()函式的,因為在普通的類中幾乎用不到,這一點和Java是相同的,但是Java中在定義純粹的資料類時,會用到get()set()函式,但是Kotlin專門這種情況定義了資料類,這個特徵。而資料類是系統已經為我們實現了get()set()函式。

這裡為大家演示getter()setter()

class Test{
    
    /*
     * getter 和 setter是可選的
     */
    
    // 當用var修飾時,必須為屬性賦預設值(特指基本資料型別,因為自定義的型別可以使用後期初始化屬性,見後續) 即使在用getter()的情況下,不過這樣寫出來,不管我們怎麼去改變其值,其值都為`123`
    var test1 : String = ""
        get() = "123"
        set(value){field = value}
    
    // 用val修飾時,用getter()函式時,屬性可以不賦預設值。但是不能有setter()函式。
    val test2 : String  
        get() = "123"       // 等價於:val test2 : String = "123"
}
複製程式碼

注意: 請認真看上面的註釋......

在上面的程式碼中出現了set(value){field = value}這樣的一句程式碼。其中valueKoltinsetter()函式時其引數的約定俗成的習慣。你也可以換成其他的值。而field是指屬性本身。

2.1、自定義

這裡講解屬性的自定義getter()setter()。由上面可知,使用val修飾的屬性,是不能有setter()的。而使用var修飾的屬性可以同時擁有自定義的getter()setter()。通過兩個例項來說明:

例1:用val修飾的屬性自定義情況

class Mime{
    // size屬性
    private val size = 0    
    
    // 即isEmpty這個屬性,是判斷該類的size屬性是否等於0
    val isEmpty : Boolean
        get() = this.size == 0

    // 另一個例子
    val num = 2
        get() = if (field > 5) 10 else 0
}

// 測試
fun main(args: Array<String>) {
    val mime = Mime()
    println("isEmpty = ${mime.isEmpty}")
    println("num = ${mime.num}")
}
複製程式碼

輸出結果為:

isEmpty = true
num = 0
複製程式碼

例2:用var修飾的屬性自定義情況

class Mime{

    var str1 = "test"
        get() = field   // 這句可以省略,kotlin預設實現的方式
        set(value){
            field = if (value.isNotEmpty()) value else "null"
        }

    var str2 = ""
        get() = "隨意怎麼修改都不會改變"
        set(value){
            field = if (value.isNotEmpty()) value else "null"
        }
}

// 測試
fun main(args: Array<String>) {
    val mime = Mime()
    
    println("str = ${mime.str1}")
    mime.str1 = ""
    println("str = ${mime.str1}")
    mime.str1 = "kotlin"
    println("str = ${mime.str1}")

    println("str = ${mime.str2}")
    mime.str2 = ""
    println("str = ${mime.str2}")
    mime.str2 = "kotlin"
    println("str = ${mime.str2}")
} 
複製程式碼

輸出結果為:

str = test
str = null
str = kotlin
str = 隨意怎麼修改都不會改變
str = 隨意怎麼修改都不會改變
str = 隨意怎麼修改都不會改變
複製程式碼

經過上面的例項,我總結出了以下幾點:

  1. 使用了val修飾的屬性,不能有setter().
  2. 不管是val還是var修飾的屬性,只要存在getter(),其值再也不會變化
  3. 使用var修飾的屬性,可以省略掉getter(),不然setter()毫無意義。當然get() = field除外。而get() = fieldKoltin預設的實現,是可以省略這句程式碼的。

故而,在實際的專案開發中,這個自定義的gettersetter的意義不是太大。

2.2、修改訪問器的可見性

如果您對Kotlin中的可見性修飾符還不瞭解的話,請參見Kotlin——中級篇(三):可見性修飾符詳解

修改屬性訪問器在實際的開發中其實也沒有太大的作用,下面舉個例子就明白了:

例:

var str1 = "kotlin_1"
    private set         // setter()訪問器的私有化,並且它擁有kotlin的預設實現

var test : String?
    @Inject set         // 用`Inject`註解去實現`setter()`
    
val str2 = "kotlin_2"
    private set         // 編譯錯誤,因為val修飾的屬性,不能有setter

var str3 = "kotlin_3"
    private get         // 編譯出錯,因為不能有getter()的訪問器可見性

fun main(args: Array<String>) {
    // 這裡虛擬碼
    str1 = "能不能重新賦值呢?"     // 編譯出錯,因為上面的setter是私有的
}
複製程式碼

如果,屬性訪問器的可見性修改為private或者該屬性直接使用private修飾時,我們只能手動提供一個公有的函式去修改其屬性了。就像Java中的BeansetXXXX()

三、後備欄位

Kotlin的類不能有欄位。 但是,有時在使用自定義訪問器時需要有一個後備欄位。為了這些目的,Kotlin提供了可以使用欄位識別符號訪問的自動備份欄位

例:

var count = 0   // 初始化值會直接寫入備用欄位
    set(value){
        field = if(value > 10) value else 0  // 通過field來修改屬性的值。
    }
複製程式碼

注意:

  • field識別符號只能在屬性的訪問器中使用。這在上面提到過
  • 如果屬性使用至少一個訪問器的預設實現,或者自定義訪問器通過field識別符號引用它,則將為屬性生成後備欄位。

看上面的例子,使用了getter()的預設實現。又用到了field識別符號。

例:不會生成後備欄位的屬性

val size = 0

/*
    沒有後備欄位的原因:
    1. 並且`getter()`不是預設的實現。沒有使用到`field`識別符號
    2. 使用`val`修飾,故而不存在預設的`setter()`訪問器,也沒有`field`修飾符
*/
val isEmpty :Boolean
    get() = this.size == 0
複製程式碼

不管是後備欄位或者下面的後備屬性,都是Kotlin對於空指標的一種解決方案,可以避免函式訪問私有屬性而破壞它的結構。

這裡值得強調的一點是,setter()

四、後備屬性

所謂後備屬性,其實是對後備欄位的一個變種,它實際上也是隱含試的對屬性值的初始化宣告,避免了空指標。

我們根據一個官網的例子,進行說明:

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // 初始化
        }
        // ?: 操作符,如果_table不為空則返回,反之則丟擲AssertionError異常
        return _table ?: throw AssertionError("Set to null by another thread")
    }
複製程式碼

從上面的程式碼中我們可以看出:_table屬性是私有的,我們不能直接的訪問它。故而提供了一個公有的後備屬性(table)去初始化我們的_table屬性。

通俗的講,這和在Java中定義Bean屬性的方式一樣。因為訪問私有的屬性的getter和setter函式,會被編譯器優化成直接反問其實際欄位。因此不會引入函式呼叫開銷。

五、編譯時常數

在開發中,難免會遇到一些已經確定的值的量。在Java中,我們可以稱其為常量。在kotlin中,我們稱其為編譯時常數。我們可以用const關鍵字去宣告它。其通常和val修飾符連用

  • 關鍵字:const
  • 滿足const的三個條件:
    1. 物件的頂層或成員,即頂層宣告。
    2. 初始化為String型別或基本型別的值
    3. 沒有定製的getter()

例:

const val CONST_NUM = 5
const val CONST_STR = "Kotlin"
複製程式碼

關於編譯時常數這個知識點更詳細的用法,我在講解講解變數的定義這一章節時,已經詳細講解過了。這裡不做累贅。

您可以參見我的Kotlin——初級篇(二):變數、常量、註釋的使用這一篇文章

六、後期初始化屬性

通常,宣告為非空型別的屬性必須在建構函式中進行初始化。然而,這通常不方便。例如,可以通過依賴注入或單元測試的設定方法初始化屬性。 在這種情況下,不能在建構函式中提供非空的初始值設定,但是仍然希望在引用類的正文中的屬性時避免空檢查。故而,後期初始化屬性就應運而生了。

  • 關鍵字 : lateinit
  • 該關鍵字必須宣告在類的主體內,並且只能是用var修飾的屬性。並且該屬性沒有自定義的setter()getter()函式。
  • 該屬性必須是非空的值,並且該屬性的型別不能為基本型別。

例:

class Test{
    // 宣告一個User物件的屬性
    lateinit var user : User   
    
    /*
        下面的程式碼是錯誤的。因為用lateinit修飾的屬性,不能為基本型別。
        這裡的基本型別指 Int、Float、Double、Short等,String型別是可以的
    */    
    // lateinit var num : Int    
}
複製程式碼

關於後期初始化屬性這一個知識點,我在講解講解變數的定義這一章節時,已經詳細講解過了。這裡不做累贅。不過關於這一知識點,一般都是在Android開發中或者在依賴注入時會用到。

您可以參見我的Kotlin——初級篇(二):變數、常量、註釋的使用這一篇文章

七、委託屬性

要想把委託屬性這一個知識點詳細的講解明白。以及它的實現與例項都要花上很大的篇幅去講解。我會單獨抽出一篇文章去講解它,故而這裡就不做累述了。您可以參見文章Kotlin——高階篇(八):委託與委託屬性詳解

總結

在一些文章或者書籍裡面,關於Kotlin屬性/欄位的講解,不會有這麼多的知識點,並且我們在實際開發中,也有一些知識點很少去用到。這裡大家主要去理解與實踐屬性的基本使用與定義、Getter()Setter()、後期初始化屬性、以及編譯時常數這幾個點就可以了。

原始碼

如果各位大佬看了之後感覺還闊以,就請各位大佬隨便star一下,您的關注是我最大的動力。
我的個人部落格Jetictors
我的githubJetictors

歡迎各位大佬進群共同研究、探索

QQ群號:497071402

Kotlin——中級篇(二): 屬性與欄位詳解

相關文章