作者:Ole Begemann,原文鏈接,原文日期:2017-01-09
譯者:Cwift;校對(duì):walkingway;定稿:CMB
在 Swift Talk episode 31 上,Chris 和 Florian 展示了一種針對(duì) Swift 中可變的嵌套異構(gòu)字典的解決方案,這種字典是 [String:Any] 類型的。這是一個(gè)有趣的討論,我鼓勵(lì)你看看原視頻或者閱讀這篇優(yōu)秀的文字記錄。
我為準(zhǔn)備這期對(duì)話節(jié)目做了點(diǎn)微小的貢獻(xiàn),不過圍繞這個(gè)問題所進(jìn)行的一些實(shí)驗(yàn)代碼視頻并沒有提到,所以我想在這里展示給你。
一個(gè)異構(gòu)字典
讓我們從一個(gè)有著多層嵌套結(jié)構(gòu)的異構(gòu)字典入手。當(dāng)你從一個(gè) Web 服務(wù)器獲取一個(gè) JSON 數(shù)據(jù)或者從一個(gè) plist 文件中加載初始化數(shù)據(jù)時(shí),可能經(jīng)常遇到這種結(jié)構(gòu)的數(shù)據(jù):
var dict: [String: Any] = [
"language": "de",
"translator": "Erika Fuchs",
"translations": [
"characters": [
"Scrooge McDuck": "Dagobert",
"Huey": "Tick",
"Dewey": "Trick",
"Louie": "Track",
"Gyro Gearloose": "Daniel Düsentrieb",
],
"places": [
"Duckburg": "Entenhausen",
"Money Bin": "Geldspeicher",
]
]
]
Florian 和 Chris 的解決方案允許你使用下面的語法來訪問(以及修改)數(shù)組中嵌套的值:
dict[jsonDict: "translations"]?[jsonDict: "characters"]?[string: "Gyro Gearloose"]
// → "Daniel Düsentrieb"
使用鍵路徑作為字典的下標(biāo)
我想要引入一個(gè)類似于 Cocoa 中 KVC 使用的鍵路徑的語法。結(jié)果看起來應(yīng)該像這樣:
dict[keyPath: "translations.characters.Gyro Gearloose"]
// → "Daniel Düsentrieb"
我們不能使用 Swift 中現(xiàn)有的 #keyPath 結(jié)構(gòu),因?yàn)樗鼤?huì)在編譯時(shí)檢查鍵路徑所引用的屬性是否存在,這不可能應(yīng)用到字典中。
KeyPath 類型
讓我們用一個(gè)新的類型來表示鍵路徑。它使用路徑分段的數(shù)組來保存鍵路徑,并且有一個(gè)便捷方法可以分離當(dāng)前的首路徑,稍后我們就會(huì)用到。
struct KeyPath {
var segments: [String]
var isEmpty: Bool { return segments.isEmpty }
var path: String {
return segments.joined(separator: ".")
}
/// 分離首路徑并且
/// 返回一組值,包含分離出的首路徑以及余下的鍵路徑
/// 如果鍵路徑?jīng)]有值的話返回nil。
func headAndTail() -> (head: String, tail: KeyPath)? {
guard !isEmpty else { return nil }
var tail = segments
let head = tail.removeFirst()
return (head, KeyPath(segments: tail))
}
}
將這個(gè)功能添加到一個(gè)自定義的類型中不是絕對(duì)必要的;畢竟,我們?cè)谔幚?a target="_blank" rel="nofollow">字符串類型的數(shù)據(jù),所以這個(gè)方案中沒有增加太多的類型安全方面的保護(hù)。提取字符串解析的代碼很方便,所以不必在字典的下標(biāo)中處理它。
說到解析,我們需要一個(gè)構(gòu)造器,它接受一個(gè)鍵路徑并且將其轉(zhuǎn)換為內(nèi)部使用的數(shù)組的表示形式:
import Foundation
///使用 "this.is.a.keypath" 這種格式的字符串初始化一個(gè) KeyPath
extension KeyPath {
init(_ string: String) {
segments = string.components(separatedBy: ".")
}
}
下一步是遵守 ExpressibleByStringLiteral 協(xié)議。這樣我們就可以使用一個(gè)諸如 “this.is.a.key.path” 這種純粹的字符串字面量來創(chuàng)建一個(gè)鍵路徑了。這個(gè)協(xié)議包含了三個(gè)必須實(shí)現(xiàn)的構(gòu)造器,所有的構(gòu)造器都代理給我們剛剛定義的那個(gè)構(gòu)造器:
extension KeyPath: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(value)
}
init(unicodeScalarLiteral value: String) {
self.init(value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
字典下標(biāo)
現(xiàn)在,該給字典寫一個(gè)擴(kuò)展了。鍵路徑只對(duì)鍵為字符串的字典有意義。不幸的是,在擴(kuò)展包含泛型參數(shù)的對(duì)象時(shí),Swift 3.0 不支持在擴(kuò)展時(shí)選擇泛型的具體類型,例如這樣的格式:extension Dictionary where Key == String。不過這個(gè)特性已經(jīng)實(shí)現(xiàn)了,并將成為 Swift 3.1 中的一部分。
在此之前,我們可以定義一個(gè)虛擬的協(xié)議,然后讓 String 遵守這個(gè)協(xié)議,以便繞過這個(gè)限制:
//因?yàn)?Swift 3.0 不支持根據(jù)具體類型進(jìn)行擴(kuò)展 (extension Dictionary where Key == String)
//所以這樣做是必須的。
protocol StringProtocol {
init(string s: String)
}
extension String: StringProtocol {
init(string s: String) {
self = s
}
}
現(xiàn)在可以用 where Key: StringProtocol 來限制擴(kuò)展了。我們將向 Dictionary 中新增一個(gè)下標(biāo),傳入一個(gè) KeyPath,返回一個(gè)可選型的 Any。下標(biāo)需要一個(gè) getter 和一個(gè) setter,因?yàn)槲覀兿胍ㄟ^鍵路徑來修改字典的值:
extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
get {
// ...
}
set {
// ...
}
}
}
下面是 getter 的實(shí)現(xiàn)方式:
extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
get {
switch keyPath.headAndTail() {
case nil:
// 鍵路徑為空。
return nil
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
// 到達(dá)了路徑的尾部。
let key = Key(string: head)
return self[key]
case let (head, remainingKeyPath)?:
// 鍵路徑有一個(gè)尾部,我們需要遍歷。
let key = Key(string: head)
switch self[key] {
case let nestedDict as [Key: Any]:
// 嵌套的下一層是一個(gè)字典
// 用剩下的路徑作為下標(biāo)繼續(xù)取值
return nestedDict[keyPath: remainingKeyPath]
default:
// 嵌套的下一層不是字典
// 鍵路徑無效,中止。
return nil
}
}
}
// ...
它需要處理四種情況:
- 如果鍵路徑是空的,返回
nil。這種情況只有當(dāng)我們處理空的鍵路徑的時(shí)候才會(huì)發(fā)生。 - 如果鍵路徑只有一個(gè)路徑段,使用基礎(chǔ)的字典下標(biāo)返回該鍵所對(duì)應(yīng)的值(如果鍵不存在則返回
nil)。 - 如果鍵路徑上的路徑段超過一個(gè),檢查是否存在可以繼續(xù)遍歷的嵌套字典。如果存在,使用剩余的路徑段遞歸調(diào)用下標(biāo)。
- 如果沒有嵌套字典,則鍵路徑的格式錯(cuò)誤。返回
nil。
setter 具有類似的結(jié)構(gòu):
extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
// ...
set {
switch keyPath.headAndTail() {
case nil:
// 鍵路徑為空。
return
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
// 直達(dá)鍵路徑的末尾。
let key = Key(string: head)
self[key] = newValue as? Value
case let (head, remainingKeyPath)?:
let key = Key(string: head)
let value = self[key]
switch value {
case var nestedDict as [Key: Any]:
// 鍵路徑的尾部需要遍歷
nestedDict[keyPath: remainingKeyPath] = newValue
self[key] = nestedDict as? Value
default:
// 無效的鍵路徑
return
}
}
}
}
}
用到的代碼相當(dāng)?shù)亩?,但它們被很好地安置到了擴(kuò)展當(dāng)中。并且調(diào)用時(shí)的格式讀起來非常優(yōu)雅,這才是最重要的。
你的目標(biāo)是讓每個(gè) API 清晰地表達(dá)出它們的用處。
—— Swift API 設(shè)計(jì)指南
這有一個(gè)例子:
dict[keyPath: "translations.characters.Gyro Gearloose"]
// → "Daniel Düsentrieb"
dict[keyPath: "translations.characters.Magica De Spell"] = "Gundel Gaukeley"
dict[keyPath: "translations.characters.Magica De Spell"]
// → "Gundel Gaukeley"
我們可以訪問值以及分配新的值。
可變的方法
下標(biāo)返回的類型是 Any?。這意味著在對(duì)返回值做任何有用的操作之前,你總是要先把它轉(zhuǎn)換成特定類型。這與值類型為 Any 的異構(gòu)字典的默認(rèn)下標(biāo)沒有區(qū)別。
正如 Chris 和 Florian 在視頻中所展示的那樣,一個(gè)很有意義的問題是改變字典中的值(不是分配一個(gè)新的值)變得非常困難,因?yàn)槟悴荒芡ㄟ^轉(zhuǎn)換類型來改變值。下面的兩行代碼都不能通過編譯:
// error: value of type 'Any' has no member 'append'
dict[keyPath: "translations.characters.Scrooge McDuck"]?.append(" Duck")
// error: cannot use mutating member on immutable value of type 'String'
(dict[keyPath: "translations.characters.Scrooge McDuck"] as? String)?.append(" Duck")
想讓這樣的代碼可以運(yùn)行,我們需要一個(gè)返回 String? 的下標(biāo)。最好的辦法是讓下標(biāo)變成泛型的,但是下標(biāo)不支持泛型。另一個(gè)最佳方案是為我們想要支持的類型添加各自參數(shù)標(biāo)簽的下標(biāo)。實(shí)現(xiàn)部分可以轉(zhuǎn)發(fā)到現(xiàn)有的下標(biāo),缺點(diǎn)是必須手動(dòng)添加每一個(gè)需要的類型。以下是字符串和字典的兩種下標(biāo):
extension Dictionary where Key: StringProtocol {
subscript(string keyPath: KeyPath) -> String? {
get { return self[keyPath: keyPath] as? String }
set { self[keyPath: keyPath] = newValue }
}
subscript(dict keyPath: KeyPath) -> [Key: Any]? {
get { return self[keyPath: keyPath] as? [Key: Any] }
set { self[keyPath: keyPath] = newValue }
}
}
現(xiàn)在下面的代碼可以運(yùn)行了:
dict[string: "translations.characters.Scrooge McDuck"]?.append(" Duck")
dict[keyPath: "translations.characters.Scrooge McDuck"]
// → "Dagobert Duck"
dict[dict: "translations.places"]?.removeAll()
dict[keyPath: "translations.places"]
// → [:]
結(jié)論
如果你經(jīng)常使用弱類型的異構(gòu)字典,應(yīng)該質(zhì)疑你的數(shù)據(jù)模型。大多數(shù)情況下,將這些數(shù)據(jù)轉(zhuǎn)換成一個(gè)自定義的結(jié)構(gòu)體或者枚舉,同時(shí)讓其滿足你的域模型并且提供更多的類型安全,這可能是一個(gè)更好的主意。
然而,在罕見的情況下,使用一個(gè)完整的數(shù)據(jù)結(jié)構(gòu)可能會(huì)矯枉過正,我真的很喜歡這里提出的方法的靈活性和可讀性。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問 http://swift.gg。