Swift Properties 的兩三事

TL;DR, 簡單做個整理 Property 的分類與特性:

  • Stored 保存變量或常量的屬性
  • Computed 單純用來運算不做保存的屬性,
  • 可自訂 setter, getter 定義屬性要設定或取得的值
  • 可使用 willSet, didSet 來觀察或執行 set 前後可做的事
  • 可使用 @propertyWrapper 的語法糖來包裝邏輯並減少代碼重複

Stored Properties

Stored Property 是作為特定 classes 或 strucutres 的實例的一部分存儲的常量或變量。 存儲屬性可以是 mutable 存儲屬性(由 var 關鍵字引入)或 const 存儲屬性(由 let 關鍵字引入)。

建立一個 Person 的 struct

struct Person {
    var weight: Double // kg
    var height: Double // cm
}

這邊我們定義的 model 是 struct,要注意若使用 let 來定義的話,他的 property 就無法被存取,會有 error 產生,這是因為 struct 是 value type,只要 instance 是定量,則他的 properties 也都會是,就算標記為 var 也一樣

// 用 let 定義 p 
let p = Person(weight: 50, height: 170)
// 這會有 error: Cannot assign to property: 'p' is a 'let' constant
p.weight = 70 

Lazy modifier

lazy 修飾符用來表示該屬性當第一次被使用時才會初始化,滿大的好處是節省記憶體得使用,不讓還未使用到的變量充斥在程式裡面,lazy 的屬性只能用 var 來定義,不能用 let ,因為 const 在初始之前就一定要有值

struct Person {
    ...
    // 定義一個 name 的 lazy var
    lazy var name: String = {
       return "Joe"
    }()
}

var p = Person(weight: 50, height: 170)
dump(p)

把 p dump 出來後會得到 nil,因為 name 尚未被存取過還沒初始化

▿ Person
  - weight: 50.0
  - height: 170.0
  - $__lazy_storage_$_name: nil

接著調用 p 的 name 就會得到已初始的值

var p = Person(weight: 50, height: 170)
print("p.name: \(p.name)") // 調用 p.name
dump(p)

這時會得到被初始化過的 name

▿ Person
  ...
  ▿ $__lazy_storage_$_name: Optional("Joe")
    - some: "Joe"

Computed Properties

  • 這些屬性並不存儲 value,它們提供了一個 getter 和一個 optional setter,以間接檢索和設置其他屬性和值。
  • 若不設定 setter 則為 read-only
  • class, structenum 皆可定義

Setter 的縮寫宣告

newValue 為預設的 setter new value

set {
    origin.x = newValue.x - (size.width / 2)
    origin.y = newValue.y - (size.height / 2)
}

Getter 的縮寫宣告

若括號內的定義是一行可以表示的話,則 return 可省略(implicitly expression)

get {
    Point(x: origin.x + (size.width / 2),
          y: origin.y + (size.height / 2))
}

試試看在 Person 裡加個計算 bmi 的 computed property

struct Person {
    ...
    // 如果只有 getter 的話 get{} 可以省略
    var bmi: Double {
        // 把公分轉成公尺
        let m = height / 100
        // 帶入 bmi 公式並 rounded digits
        return round(1000 * (weight / (m * m))) / 1000
    }
}

let p = Person(weight: 50, height: 170)
print("p.bmi: \(p.bmi)")
// 會得到計算結果:p.bmi: 17.301

Property Observers

觀察並回應屬性的變化。 每次設置屬性值時都會調用 Property Observer,即使新值與屬性的當前值相同也是如此。

  • 可在任一個 stored property 設置,但無法在 lazy 的 property 設置。
  • 可在任一個 inherited property 設置(stored or computed),不過若 computed property 為非覆寫 (nonoverridden) 的話則不需要設置,因為可直接在他的 setter 做出相對的 responed 即可。
  • willSet 會在值保存之前調用,newValue 為預設的變數
  • didSet 則是在值保存之後調用,oldValue 為預設的變數

Example:

struct Person {
    var weight: Double // kg
    var height: Double // cm
    lazy var name: String = {
       return "Joe"
    }()
    // 如果只有 getter 的話 get{} 可以省略
    var bmi: Double {
        // 把公分轉成公尺
        let m = height / 100
        return round(1000 * (weight / (m * m))) / 1000
    }
    // 減重計畫
    var lossWeight: Double = 0 {
        willSet(newValue) {
            print("將減重 \(newValue) 公斤")
        }
        didSet {
            print("總共減重 \(lossWeight + oldValue) 公斤")
            weight -= lossWeight
        }
    }
}

var p = Person(weight: 70, height: 170)
print("減重前的 bmi: \(p.bmi)")
// 減重前的 bmi: 24.221
p.lossWeight = 3
// 將減重 3.0 公斤
// 總共減重 3.0 公斤
p.lossWeight = 5
// 將減重 5.0 公斤
// 總共減重 8.0 公斤
print("減重後的 bmi: \(p.bmi)")
// 減重後的 bmi: 21.453

Property Wrapper

Wrapper 是一個提供可自定義和保存 property 方式的語法糖,方便使用並減少代碼的重複

  • 使用 @propertyWrapper keyword
  • class, structenum 皆可以定義
  • 需定義一個 non-static wrappedValue 的 property

直接看範例

@propertyWrapper
struct StandardBmi {
    private var bmi: Double
    // 需定義 wrappedValue
    var wrappedValue: Double {
        get { return bmi }
        // 標準 BMI 的範圍為 18.5 <= bmi < 24 之間
        set { bmi = 18.5..<24 ~= newValue ? newValue : 0 }
    }
    init() { self.bmi = 0 }
}

// 標準 BMI 的 Person
struct StandardPerson {
    @StandardBmi var bmi: Double
}

var s = StandardPerson()
print("s.bmi: \(s.bmi)")
// 設定標準 bmi
s.bmi = 20.1
print("s.bmi: \(s.bmi)") // s.bmi: 20.1
// 設定範圍外的 bmi 都不會設定成功
s.bmi = 16.6
print("s.bmi: \(s.bmi)") // s.bmi: 0.0
s.bmi = 25.8
print("s.bmi: \(s.bmi)") // s.bmi: 0.0

Wrapped Properties 的初始化

我們可以幫 StandardBmi 增添更多初始化方式

@propertyWrapper
struct StandardBmi {
    ...
    // 自訂初始化
    init(wrappedValue: Double) {
        bmi = 18.5..<24 ~= wrappedValue ? wrappedValue : 0
    }
}

// 標準 BMI 的 Person
struct StandardPerson {
    // Wrapper Property 就可以這樣寫
    @StandardBmi(wrappedValue: 20) var bmi: Double
    // 當然也可以直接這樣寫
    // @StandardBmi var bmi: Double = 20
}

Projecting a value

Property Wrapper 還有一個功能就是可以定義投射值 (projected value)

  • 定義 projectedValue 的 property
  • 透過 $ (dollar sign) 來存取投射值

假設我們希望 StandardBmi 在設定 bmi 參數的時候可以提示是否有設置成功,可以這樣寫

@propertyWrapper
struct StandardBmi {
    private var bmi: Double
    // 要投射的變數
    var projectedValue: Bool
    // 需定義 wrappedValue
    var wrappedValue: Double {
        get { return bmi }
        // 標準 BMI 的範圍為 18.5 <= bmi < 24 之間
        set {
            if 18.5..<24 ~= newValue {
                bmi = newValue
                projectedValue = true
            } else {
                bmi = 0
                projectedValue = false
            }
        }
    }
    init() {
        bmi = 0
        projectedValue = false
    }
}

// 標準 BMI 的 Person
struct StandardPerson {
    @StandardBmi var bmi: Double
}

var s = StandardPerson()
s.bmi = 20
print("\(s.$bmi)") // true
s.bmi = 15
print("\(s.$bmi)") // false

References

comments powered by Disqus