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) 包裹起來,只開放一個或兩個公開的窗口 (圖中的 wrappedValueprojectedValue) 來存取這個資料的結構 (通常是 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

@State 變數projectedValue 所傳回的值是 @State 變數本身 (self)。 👉 Property Wrappers in Swift - Swift by Sundell

The @Published property wrapper uses its projectedValue to expose a publisher. 👉 Wrapping your head around Property Wrappers in Swift - Donny Wals

Last updated