結構體與 NSCoding

AndersonSma 7年前發布 | 7K 次閱讀 iOS開發 程序員 移動開發

要使用 NSCoding ,必須遵循 NSObjectProtocol 這個類協議,因此結構體無法使用。如果我們想對某些數據進行編碼,最簡單的方式是將它們作為一個類來實現,并且繼承自 NSObject 。

我找到了一種優雅的方式來將結構體包在 NSCoding 的容器中,存儲時也不會讓人覺得小題大做。用 Coordinate 舉個例子:

struct Coordinate: JSONInitializable {
    let latitude: Double
    let longitude: Double

    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}

這是一個簡單的類型,帶有兩個常量屬性。接下來我將創建一個遵循 NSCoding 協議的類,并將 Coordinate 包在其中:

class EncodableCoordinate: NSObject, NSCoding {

    var coordinate: Coordinate?

    init(coordinate: Coordinate?) {
        self.coordinate = coordinate
    }

    required init?(coder decoder: NSCoder) {
        guard
            let latitude = decoder.decodeObject(forKey: "latitude") as? Double,
            let longitude = decoder.decodeObject(forKey: "longitude") as? Double
            else { return nil }
        coordinate = Coordinate(latitude: latitude, longitude: longitude)
    }

    func encode(with encoder: NSCoder) {
        encoder.encode(coordinate?.latitude, forKey: "latitude")
        encoder.encode(coordinate?.longitude, forKey: "longitude")
    }
}

把以上的邏輯放在另一個類型中是合情合理的,這樣可以更嚴格地適用單一職責原則(single responsibility principle)。聰明的讀者在閱讀上面的類時,會發現 EncodableCoordinate 類中的 coordinate 這一屬性是 Optional 的,但也可以不這樣實現。我們可以使對應的構造器接收一個非 Optional 的 Coordiante 參數(或使用可失敗構造器),而 init(coder:) 構造器原本就是可失敗的,現在如果能得到一個 EncodableCoordinate 類的實例,可以保證該實例中總有 coordinate 。

然而由于 NSCoder 工作方式的特殊性,當編碼 Double 類型(以及其他基本類型)時,這些類型的數據無法使用 decodeObject(forKey:) 方法來進行解碼(這樣做會返回 Any? ),而是需要使用它們專屬的方法,對 Double 來說,則是 decodeDouble(forKey:) 。不幸的是,這些專屬方法不會返回 Optional,在找不到 key 或碰到其他類型的錯誤時會返回 0.0 。因此,我選擇將 coordinate 屬性實現為 Optional,并作為 Optional 來編碼,從而在使用 decodeObject(forKey:) 方法來進行解碼時,能獲取 Double? 類型的對象,并添加一些額外的安全性。

從現在開始,我們可以創建 EncodableCoordinate 的實例,用它來編解碼 Coordinate 對象,并通過 NSKeyedArchiver 寫入磁盤:

let encodable = EncodableCoordinate(coordinate: coordinate)
let data = NSKeyedArchiver.archiveRootObject(encodable, toFile: somePath)

存儲時每次都創建一個額外的對象未免太麻煩了,并且我也希望將這種方法和 SKCache (來源于 Cache Me If You Can 這篇文章)一起使用,如果我能規范編碼器與被編碼對象之間的關系,也許就能避免每次都創建一個 NSCoding 容器。

想要做到這一點,先添加兩個協議:

protocol Encoded {
    associatedtype Encoder: NSCoding

    var encoder: Encoder { get }
}

protocol Encodable {
    associatedtype Value

    var value: Value? { get }
}

并讓兩個類對應遵守這兩個協議:

extension EncodableCoordinate: Encodable {
    var value: Coordinate? {
        return coordinate
    }
}

extension Coordinate: Encoded {
    var encoder: EncodableCoordinate {
        return EncodableCoordinate(coordinate: self)
    }
}

實現了以上內容之后,類型系統就知道如何在這些對象對之間進行值的轉換了。

class Cache<T: Encoded> where T.Encoder: Encodable, T.Encoder.Value == T {
    //...
}

對上文中提到的 SKCache 對象進行了升級之后,它現在更具通用性,可以在符合 Encoded 協議的類型中使用了。同時它也約束了該類型的編碼器的 value 對象類型必須是該類型本身,使得兩個類型之間可以進行雙向轉換。

最后需要完善的一部分是該類型的 save 與 fetch 方法。 save 包括了獲取 encoder (真正遵守 NSCoding 協議的對象),并將其存到某個路徑中:

func save(object: T) {
   NSKeyedArchiver.archiveRootObject(object.encoder, toFile: path)
}

fetch 則包括了一些微小的編譯器工作。我們需要將解檔對象的類型轉換為 T.Encodable ,即編碼器的類型,然后獲取它的值,并動態將其類型轉換回 T 。

func fetchObject() -> T? {
    let fetchedEncoder = NSKeyedUnarchiver.unarchiveObject(withFile: storagePath)
    let typedEncoder = fetchedEncoder as? T.Encoder
    return typedEncoder?.value as T?
}

現在,要使用這個 cache,只需要實例化一個對象并指定其類型為 Coordinate :

let cache = Cache<Coordinate>(name: "coordinateCache")

生成了該對象之后,我們就可以透明地存取 coordinate 結構體了:

cache.save(object: coordinate)

使用以上方法,我們可以通過 NSCoding 來編碼結構體,遵守單一職責原則,并加強了類型安全。

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

 

來自:http://swift.gg/2017/03/09/structs-and-nscoding/

 

 本文由用戶 AndersonSma 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!