使用鍵路徑訪問字典

作者: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
                }
            }
        }
        // ...

它需要處理四種情況:

  1. 如果鍵路徑是空的,返回 nil。這種情況只有當(dāng)我們處理空的鍵路徑的時(shí)候才會(huì)發(fā)生。
  2. 如果鍵路徑只有一個(gè)路徑段,使用基礎(chǔ)的字典下標(biāo)返回該鍵所對(duì)應(yīng)的值(如果鍵不存在則返回 nil)。
  3. 如果鍵路徑上的路徑段超過一個(gè),檢查是否存在可以繼續(xù)遍歷的嵌套字典。如果存在,使用剩余的路徑段遞歸調(diào)用下標(biāo)。
  4. 如果沒有嵌套字典,則鍵路徑的格式錯(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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young閱讀 4,169評(píng)論 1 10
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,037評(píng)論 4 61
  • 文/魚兒君 Z城是山溝溝里的一座小縣城。 2001年,柳欣一家從村里撤到城里,柳蕓和阿恒成為鄰居。 01 “嘿,妖...
    魚兒君閱讀 825評(píng)論 8 8
  • 一個(gè)人生活久了,自然會(huì)懷戀從前。 或是事,或是物;或會(huì)微笑,或會(huì)惆悵,但不論怎樣,至少有回憶可回,有往事可念。 去...
    荋安閱讀 299評(píng)論 1 0
  • 我是一位孤獨(dú)的漂泊者,但漂泊的城市卻給我無比溫暖! 孤獨(dú)這個(gè)詞這段時(shí)間火了,大家共鳴于孤獨(dú)這種現(xiàn)狀,都感慨自己...
    阿毅閱讀 617評(píng)論 0 1

友情鏈接更多精彩內(nèi)容