如何使用 Properties 和 Fields

如何使用 Properties 和 Fields

在 Kotlin 裡,你可以宣告一個類別,裡面可以放入 mutable(可變的) 跟 immutable(不可變的) 的成員變數,分別以 var 以及 val 代表,var 代表著可變的,也就是說如果你宣告完一個值給這個變數以後,之後仍然可以改變這個變數的值,val 則是相反,一但確定了值,就不能再進行變動了。

說明

假設你宣告一個類別,就可以如下面範例所示。

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

如果你要使用屬性,可以透過物件,在 Kotlin 宣告一個物件,跟 Java 不太一樣的是不需要加上 new 這個關鍵字,然而如果要使用屬性,就直接透過物件取得即可。

fun copyAddress(address: Address): Address {
    val result = Address() // there's no 'new' keyword in Kotlin
    result.name = address.name // accessors are called
    result.street = address.street
    // ...
    return result
}

在 Kotlin 裡面,一個屬性內建完整的宣告如下。

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

可以看到分成幾個部分,其中 property_initializer 取決於 PropertyType 是否要寫,如果有初始化 property_initializer 那麼就不用宣告 PropertyType,反之則可以宣告也可以不宣告,getter 跟 setter 則是可以自由選擇要不要設定。

// error: explicit initializer required, default getter and setter implied
var name: String
// has type Int, default getter and setter
var initialized = 1 

這邊可以看到如果你有設定 PropertyType 那麼就要給定一個初始值,否則會發生錯誤,如果你想要後面再設定則可以使用 lateinit 來處理,如果你連 PropertyType 都沒有設定,只需要給定相對應的值,則會自動幫你做型態的判定。

val simple: Int? // has type Int, default getter, must be initialized in constructor
val inferredType = 1 // has type Int and a default getter

如果你是透過 val 的方式宣告變數,那麼預設只有 getter 可以使用。

val isEmpty: Boolean
    get() = this.size == 0

如果你是透過 var 的方式宣告變數,那麼就可以客製化自己的 getter / setter,其中你可以使用你喜歡的參數名稱來代表 setter 的傳入參數。

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }

你也可以自定義私有的 setter 或者透過 inject 工具來實作。

var setterVisibility: String = "abc"
    private set // the setter is private and has the default implementation

var setterWithAnnotation: Any? = null
    @Inject set // annotate the setter with Inject

Backing Fields

backing field 是什麼?

他其實來自於 Kotlin 的一個特性,當有一段程式碼如下所示。

var name: String? = null
  set(value) {
    name = value
  }
  get() = name

看起來合情合理,但是實際上你如果執行看看會發現掉入無窮迴圈,這是為什麼呢?原因就出在 Kotlin 只要去存取 property 的時候,都會先呼叫 get(),這樣一來當我們在 set 這邊要取得 name 這個 property,就會去呼叫 get(),這樣一來就會變成下面這樣。

get() = get() = get() ...

無限跑下去的情況,程式就會掛掉,所以這時候我們就需要用到 backing field,將程式改成以下這樣。

var name: String? = null
  set(value) {
    field = value
  }
  get() = field

這時候就可以正常運作,這樣就是 backing field 存在的理由。

Backing Properties

如果你有一個屬性想要封裝起來,你可以宣告另外一個變數來指向原本的私有變數成員,這樣一來,只能透過 getter 的方式取得這個屬性值。

private var _table: Map<String, Int>? = null
public 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")
    }

Compile-Time Constants

如果你要宣告常數項,在 Java 內通常會宣告成 final static ,如下方所示。

final static String TAG = "TAG_CONST";

在 Kotlin 內可以加入 const 關鍵字,稱作 compile time constants,有以下幾個原則。

companion object Factory {
  const val TAG = "tag"
}

或者宣告在 object 宣告的 singleton 內部。

object Factory { 
	const val c = 1 
}

Late-Initialized Properties and Variables

你可以透過 lateinit 關鍵字來讓變數晚一點設定值。

class Person() {
    private lateinit var name: String
    fun setMyName(myName: String) {
        name = myName
    }
}

Checking whether a lateinit var is initialized (since 1.2)

你可以透過 isInitialized 來判斷該變數是否被初始化。

參考資料

https://kotlinlang.org/docs/reference/properties.html