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
,struct
和enum
皆可定義
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
,struct
跟enum
皆可以定義- 需定義一個 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