🔰property wrapper
把真正的資料包裹起來,只開放一個或兩個公開窗口的結構。
Swift ⟩ type ⟩ property ⟩ wrapper
A property wrapper is a simple way to apply a common pattern of behavior to a property, it's a way of annotating the property that defines how it will be stored or computed on reading.
built-in property wrappers
緣起
如果我們要設計一個「學生」的結構,然後有「分數」這個屬性,但規定不管我們如何輸入這個分數,這個分數都只能介於 0 ~ 100 之間。例如:
我們輸入的分數
學生實際的分數
說明
-200
0
最低只能 0 分。
60
60
如果是正常的分數,就不改變。
300
100
最高只能 100 分。
如果輸入程式碼的話,要長這樣:
var student = Student()
student.grade = 500
print(student.grade) // 結果是:100
student.grade = 60
print(student.grade) // 結果是:60
student.grade = -20
print(student.grade) // 結果是:0
這時要如何設計這個結構 (struct) 呢?首先,嘗試下面的程式碼:
struct Student {
// 1. 使用 private var 儲存分數,不讓外部存取。
private var _grade: Int = 0
// 2. 用 computed property 來控管分數的輸入輸出 (get/set)。
var grade: Int {
// 2.1 (get) 輸出不控管,直接回傳內部儲存的分數。
get { _grade }
// 2.2 (set) 輸入要控管,只儲存 0 ~ 100 之間的分數
set {
_grade =
newValue < 0 ? 0 :
newValue > 100 ? 100 :
newValue
}
}
}
只要這樣設計,加入一個「私密變數」(private var)、再一個「計算屬性」(computed property),就可以達到我們原先的要求:「可以控管學生的分數始終在 0 ~ 100 之間」。
任務完美達成,還有問題嗎❓
這樣做雖然沒什麼問題,但問題在於:在程式設計中,類似這樣的模式 (pattern) 實在太常出現,例如:「分數在 0 ~ 100 之間」、「評分在 0 ~ 10 之間」、「pH 值在 -1 到 16 之間」、「顏色值在 0 ~ 255 之間」等等,這些一再出現的類似模式,如果每次遇到的時候,都要重新寫一次,那就違反了程式設計的 DRY 綱領:「 Don't Repeat Yourself❗️」,而這也就是為什麼 Swift 要在 5.1 的版本中加入 Property Wrapper 的功能了,Property Wrapper 可以讓我們只設計一次,就解決上面所有的類似模式,不需每次遇到時就重寫一次。
為何叫 Property Wrapper❓
你只要仔細觀察上面「控管學生分數」的例子就會發現:真正的分數是被藏在私密變數 _grade
中,外界無法直接存取這個變數,而是必須透過計算屬性(computed property) grade
來存取它,換句話說,真正的分數等於是被包起來(wrapped)了,而計算屬性 grade
某種程度說來,就是包住它的人(wrapper),而這也就是為什麼 Swift 要將這個新功能稱為:「Property Wrapper」的原因了。
下面我來說明:如何用 Property Wrapper 來達成上面所說的「控管學生的分數」。
設計第一個 Property Wrapper
首先,我們必須先設計一個可以「控管學生的分數」的結構(struct),設計的方式跟上面的計算屬性 grade
很類似:👉 程式碼:paiza.io
// ⭐️ 1. 必須用 "@propertyWrapper" 指明:
// 這個 struct 是用來「控管分數」的,
// 也就是用來包裹分數屬性的 property wrapper。
@propertyWrapper
// 2. 名字可自訂,但因為要「控管分數」,所以取名 "Grade"
struct Grade {
// 3. 真正的分數藏這裡(變數名稱可自訂)
private var _grade = 0
// ⭐️ 4. 管控與外界聯繫的「窗口」:
// 一定要稱為 "wrappedValue",這是內定關鍵字❗️
var wrappedValue: Int {
get { _grade }
set {
_grade =
newValue < 0 ? 0 :
newValue > 100 ? 100 :
newValue
}
}
}
// 現在控管分數的屬性,只要一行就可以搞定❗️
struct Student {
// ⭐️ 5. 使用 @propertyWrapper Grade 來控管這個分數
// 使用時 Grade 前面要加 "@"❗️
@Grade var grade: Int
}
var student = Student()
student.grade = 500
print(student.grade) // 結果是:100
student.grade = 60
print(student.grade) // 結果是:60
student.grade = -20
print(student.grade) // 結果是:0
這樣設計,有何好處❓
這樣設計的好處是:如果我們有很多科的分數都要登記,例如:國文、英文、數學等,在沒有 property wrapper 之前,我們必須為每一科的分數寫一個計算屬性(computed property),才能確保每個分數都在 0 ~ 100 之間。
但有了 property wrapper 之後就不需要了,因為我們已經將「確保分數都在 0 ~ 100 之間」這樣的機制寫在 @propertyWrapper Grade
之中,所以只要在每科分數的變數前面加上 @Grade
,註明我們要用相同的機制來控管這些分數就可以,如下:
struct Student {
// ⭐️ 5. 使用 @propertyWrapper Grade 來控管這些分數
// 注意:使用時 "Grade" 前面要加 "@"❗️
@Grade var math : Int
@Grade var english: Int
@Grade var chinese: Int
}
var student = Student()
student.math = 500
print(student.math) // 結果是:100
student.english = 60
print(student.english) // 結果是:60
student.chinese = -20
print(student.chinese) // 結果是:0
留下的問題
當我們用
@propertyWrapper Grade
(後面開始都簡稱為@Grade
) 來控管一個分數屬性時,Swift 到底背後做了什麼魔法❓當我們設計了
@Grade
,雖然可以控管分數,卻也只能控管「0 ~ 100」的分數。如果我們要控管其他範圍的數字,如:pH 值、顏色值等等,又該如何❓
這些問題且待我們有空時,繼續分解。🚧

⚠️ 以下內容尚未整理,請自行斟酌觀看❗️
一個 property wrapper 就是把真正的資料 (圖中的 _value
) 包裹起來,只開放一個或兩個公開的窗口 (圖中的 wrappedValue 與 projectedValue) 來存取這個資料的結構 (通常是 struct,但也可以是 class, enum 等其他型別)。

目前只能存活在別的結構裡面,當作別人的屬性,還不能獨立存在,所以稱為 Property Wrapper。
以一當三

以上圖為例,我們為 Person
這個結構添加了一個 property wrapper - @Wrapped var name
,但實際上,Swift compiler 會在背後自動產生三個屬性 (如果我們有定義 projectedValue 的話):
name:這是公開屬性,直接與 wrappedValue 對接,用
person.name
就等同於存取 property wrapper 的 wrappedValue。$name:也是公開屬性,直接與 projectedValue 對接,用
person.$name
等同於存取 property wrapper 的 projectedValue,但此值是有宣告才有,不是每個 property wrapper 都必備。_name:這是 Person 的私密屬性,代表 property wrapper 本身,只能在 Person 內部使用,在外部並不能用
person._name
來存取。
重點
只能用 var 宣告,不能用 let。
可以在別的 property wrapper 裡面當屬性來用,例如:@UnitInterval
可以用來做什麼❓
自動編碼 (Codable):👉
目前不支援
函數內區域變數
func liveView() -> some View {
// ❌ property wrappers are not yet supported on local properties
@State var text = ""
Text("Hello World")
}
廣域變數
// ❌ property wrappers are not yet supported in top-level code
@State var tmp = ""
範例
@propertyWrapper
struct SmallNumber {
// ⭐️ Property Wrapper 背後掌管的變數
private var _max : Int
private var _value: Int
// ⭐️ 要存取背後變數的統一通道 (wrappedValue)
var wrappedValue: Int {
get { return _value }
set { _value = min(newValue, _max) }
}
/*
* ⚠️ 注意:
* Swift 5.1 需要另外定義兩個 .init
* - .init()
* - .init(wrappedValue:)
*/
init(){
self._max = 12
self._value = 0
}
init(wrappedValue: Int) {
self._max = 12
self._value = min(wrappedValue, 12)
}
/*
* Swift 5.2 則不需要,只要下面這個就可以。
*/
init(wrappedValue: Int = 0, max: Int = 12) {
self._max = max
self._value = min(wrappedValue, max)
}
}
Projected Value
#todo
Last updated
Was this helpful?