1. 程式人生 > >Swift 屬性觀察器

Swift 屬性觀察器

作者:Mattt,原文連結,原文日期:2018-08-20 譯者:Hale;校對:numbbbbbpmstYousanflics;定稿:Forelax

到了 20 世紀 30 年代,Rube Goldberg 已成為家喻戶曉的名字,與 “自營餐巾” 等漫畫中描繪的奇異複雜和異想天開的發明同義。大約在同一時期,阿爾伯特·愛因斯坦對尼爾斯·玻爾量子力學的普遍解釋進行了 批判,並從中提出了“鬼魅似的遠距作用”這一詞彙。

近一個世紀之後,現代軟體開發已經被視為可能成為 Goldbergian 裝置的典範——通過量子計算機相信我們會越來越接近這個鬼魅的領域。

作為軟體開發人員,我們提倡儘可能減少程式碼中的遠端操作。這是根據一些眾所周知的規範法則得出的,如

單一職責原則最少意外原則笛米特法則。儘管它們可能會對程式碼產生一定的副作用,但更多的時候這些原則能使程式碼邏輯變得清晰。

這是本週關於 Swift 屬性觀察文章的焦點,它提出了一種內建的輕量級替代方案,適用於更正式的解決方案,如模型 - 檢視 - 檢視模型(MVVM)函式響應式程式設計(FRP)。

Swift 中有兩種屬性:儲存屬性,它們將狀態和物件相關聯;計算屬性,則根據該狀態執行計算。例如,

struct S {
    // 儲存屬性
    var stored: String = "stored"

    // 計算屬性
    var computed: String {
        return
"computed" } } 複製程式碼

當你宣告一個儲存屬性,你可以使用閉包定義一個 屬性觀察器,該閉包中的程式碼會在屬性被設值的時候執行。willSet 觀察器會在屬性被賦新值之前被執行,didSet 觀察器則會在屬性被賦新值之後執行。無論新值是否等於屬性的舊值它們都會被執行。

struct S {
    var stored: String {
        willSet {
            print("willSet was called")
            print("stored is now equal to \(self.stored)")
            print
("stored will be set to \(newValue)") } didSet { print("didSet was called") print("stored is now equal to \(self.stored)") print("stored was previously set to \(oldValue)") } } } 複製程式碼

例如,執行下面的程式碼在控制檯的輸出如下:

var s = S(stored: "first")
s.stored = "second"
複製程式碼
  • willSet was called
  • stored is now equal to first
  • stored will be set to second
  • didSet was called
  • stored is now equal to second
  • stored was previously set to first

需要注意的是當屬性在初始化方法中進行賦值時,不會觸發觀察器的程式碼。從 Swift4.2 開始,你可以將賦值邏輯包裝在 defer 程式碼塊來解決這個問題,但這是 一個很快就會被修復的問題,因此你不需要依賴於這種行為。

Swift 的屬性觀察器從一開始就是語言的一部分。為了更好地理解其原理,讓我們快速瞭解一下它在 Objective-C 中的工作原理。

Objective-C 中的屬性

從某種意義上說,Objective-C 中的所有屬性都是被計算出來的。每次通過點語法訪問屬性時,都會轉換為等效的 getter 或 setter 方法呼叫。這些呼叫最終被編譯成訊息傳送,隨後再執行讀取或寫入例項變數的方法。

// 點語法訪問
person.name = @"Johnny";

// ...等價於
[person setName:@"Johnny"];

// ...它被編譯成
objc_msgSend(person, @selector(setName:), @"Johnny");

// ...最終實現
person->_name = @"Johnny";
複製程式碼

程式設計過程中我們通常想要避免引入副作用,因為它會導致難以推斷程式的行為。但很多 Objective-C 開發者已經依賴於這種特性,他們會根據需要在 getter 或 setter 中注入各種額外的行為。

Swift 的屬性設計使這些模式更加標準化,並對裝飾狀態訪問(儲存屬性)的副作用和重定向狀態訪問(計算屬性)的副作用進行了區分。對於儲存屬性,willSetdidSet 觀察器將替換你在 ivar 訪問時的程式碼。對於計算屬性,getset 訪問器可能會替換在 Objective-C 中實現的一些 @dynamic 屬性。

正因為如此,我們才可以獲取更一致的語義,並更好地保證鍵值觀察(KVO)和健值編碼(KVC)等屬性互動機制。

那麼你可以使用 Swift 屬性觀察器做些什麼呢?以下是一些供你參考的想法:

標準化或驗證值

有時,你希望對型別確定的值增加額外的約束。

例如,你正在開發一個和政府機構對接的應用程式,你需要保證使用者填寫了所有的必填項並且不包含非法的值才能提交表單。

如果一個表單要求名稱欄位使用大寫字母且不使用重音符號,你可以使用 didSet 屬性觀察器自動去除重音符號並轉化為大寫。

var name: String? {
    didSet {
        self.name = self.name?
                        .applyingTransform(.stripDiacritics,
                                            reverse: false)?
                        .uppercased()
    }
}
複製程式碼

幸運的是在觀察器內部設定屬性不會觸發額外的回撥,所以上面的程式碼中不會產生無限迴圈。我們之所以不使用 willSet 觀察器是因為即使我們在其回撥中進行任何賦值,都會在屬性被賦予 newValue 時覆蓋。

雖然這種方法可以解決一次性問題,但像這樣需要重複使用的業務邏輯可以封裝到一個型別中。

更好的設計是建立一個 NormalizedText 型別,它封裝了要以這種形式輸入的文字的規則:

struct NormalizedText {
    enum Error: Swift.Error {
        case empty
        case excessiveLength
        case unsupportedCharacters
    }

    static let maximumLength = 32

    var value: String

    init(_ string: String) throws {
        if string.isEmpty {
            throw Error.empty
        }

        guard let value = string.applyingTransform(.stripDiacritics,
                                                   reverse: false)?
                                .uppercased(),
              value.canBeConverted(to: .ascii)
        else {
             throw Error.unsupportedCharacters
        }

        guard value.count < NormalizedText.maximumLength else {
            throw Error.excessiveLength
        }

        self.value = value
    }
}
複製程式碼

一個可丟擲異常的初始化方法可以向呼叫者傳送錯誤資訊,這是 didSet 觀察器無法做到的。現在面對 蘭韋爾普爾古因吉爾戈格里惠爾恩德羅布爾蘭蒂西利奧戈戈戈赫約翰尼 這樣的麻煩製造者,我們能為他做些什麼!(換言之,以合理的方式傳達錯誤比提供無效的資料更好)

傳播依賴狀態

屬性觀察器的另一個潛在用例是將狀態傳播到依賴於檢視控制器的元件。

考慮下面的 Track 模型示例和一個呈現它的 TrackViewController

struct Track {
    var title: String
    var audioURL: URL
}

class TrackViewController: UIViewController {
    var player: AVPlayer?

    var track: Track? {
        willSet {
            self.player?.pause()
        }

        didSet {
            guard let track = self.track else {
                return
            }

            self.title = track.title

            let item = AVPlayerItem(url: track.audioURL)
            self.player = AVPlayer(playerItem: item)
            self.player?.play()
        }
    }
}
複製程式碼

當檢視控制器的 track 屬性被賦值,以下事情會自動發生:

  • 之前軌道的音訊都會暫停
  • 檢視控制器的 title 會被設定為新軌道物件的標題
  • 新軌道物件的音訊資訊會被載入並播放

很酷, 對嗎?

你甚至可以像 捕鼠記 中描繪的場景 一樣,將行為與多個觀察屬性級聯起來。

當然,觀察器也存在一定的副作用,它使得有些複雜的行為難以被推斷,這是我們在程式設計中需要避免的。今後在使用這一特性的同時也需要注意這一點。

然而,在這搖搖欲墜的抽象塔的頂端,一定限度的系統混亂是誘人的,有時是值得的。一直遵循規則的是波爾理論而非愛因斯坦。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg