結構體與 NSCoding
要使用 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/