Effective Kotlin - 屬性應代表狀態,而非行為

Kotlin 的屬性看起來與 Java 的欄位相似。然而,它們擁有不同的概念。


//Kotlin property
var name:String?=null

//Java field
String name=null;

即使以相同的方式使用,Kotlin 的屬性也能實現更多功能。

屬性宣告的完整語法


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

自定義 Get, Set ( var )


var name: String? = null
    get() = field?.toUpperCase()
    set(value) { if(!value.isNullOrBlank()) { field = value }

自定義 Get ( val )


val fullName: String?
    get() = "$name $surname"

後備欄位 (Backing Fields)

欄位無法自定義宣告。不過,當屬性需要後備欄位時,系統會自動建立。此後備欄位可以在存取器中透過 field 識別字進行參照。


var counter = 0 // 初始化器直接賦值給後備欄位
    set(value) {
        if (value >= 0)
            field = value
            // counter = value // 錯誤 StackOverflow:使用實際名稱 'counter' 會導致 setter 遞迴呼叫
    }

與函數有何不同?

在 Kotlin 中,屬性代表該物件的狀態,而函數則代表該物件的行為


// 屬性
val isEmpty: Boolean
  get() = amount == 0


// 函數
fun isEmpty(): Boolean {
  return amount == 0
}

讓我們思考一下。

總是使用屬性的方式比較好嗎?


class FruitBucket(
    val price: Int,
    val discount: Int,
    var amount: Int
) {
  val isEmpty: Boolean
    get() = amount == 0
  val salePrice: Int
    get() = price - discount
}

若要改善上述程式碼,可以修改如下:
因為每次存取時都進行計算是沒有效率的。


class FruitBucket(
    val price: Int,
    val discount: Int,
    var amount: Int
) {
  val isEmpty: Boolean
    get() = amount == 0
  val salePrice: Int = price - discount
}

可以在不持有資料的情況下使用該屬性。

基於某些原因,即便無法使用 Date 型別,也可以利用 Kotlin 的屬性功能,不直接儲存 Date 型別的資料,而是利用其他數值來使用 Date 型別的值。(例如在進行序列化等操作時,僅持有 millis 的值,使用時再轉換為 Date 型別的形式)


var date: Date
    get() = Date(millis)
    set(value) {
        millis = value.time
    }

屬性不僅是單純的欄位,也可以作為存取器使用。


val Context.preferences:SharedPreferences
    get() = PreferenceManager.getDefaultSharedPreferences(this)
val Context.inflater:LayoutInflater
    get() = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val Context.notificationManager:NotificationManager
    get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

屬性功能是萬能的嗎?

包含迴圈、重複等特定邏輯,或是注入演算法部分 的屬性,並非正確的使用方式。


// 不要這樣做!
val Tree<Int>.sum:Int
    get() = when (this) {
        is Leaf -> value
        is Node -> left.sum + right.sum
    }

屬性 (Property) 通常應該僅用於表示狀態或進行設定。
以下提供建議使用函數而非屬性的情況:

  • 複雜度高於 O(1) 的情況(計算成本較高時)
  • 包含除了簡單操作(如記錄日誌、更新元素等)以外的業務邏輯時
  • 連續兩次呼叫成員卻輸出不同結果時
  • 會產生類似 Int.toDouble() 這種慣例表達式的重複時
  • 在 Getter 中變更屬性狀態時

參考資料

AD