Swift 中集合與字典的角逐

作者:Erica Sadun,原文鏈接,原文日期:2015-10-19
譯者:CMB;校對(duì):Cee;定稿:千葉知風(fēng)

傳統(tǒng)的 Cocoa 在使用字典時(shí)有個(gè)不好的習(xí)慣。無(wú)論是用戶信息還是字體選項(xiàng)亦或是視頻流(AVFundation)設(shè)置,NSDictionary 一直擔(dān)任 Cocoa 傳遞數(shù)據(jù)的角色。字典是靈活的、易用的,但它也存在諸多潛在的危險(xiǎn)。

在這篇文章中,我將討論另一種更加 Swift 化的方法。這并不是一個(gè)能夠徹底解決問(wèn)題的方法,但我認(rèn)為它是一個(gè)在后 Swift 時(shí)代中能夠更好展示 API 是如何工作的觀念模式。

基于字典的設(shè)置工作

下面的代碼是從我自己的一個(gè)項(xiàng)目中抽取出來(lái)的(如果你熟悉我其他的文章或許會(huì)有印象,這是我寫的 Movie Maker 類)。這幾行 Objective-C 代碼創(chuàng)建了一個(gè)名為 options 的字典,用來(lái)構(gòu)建一個(gè)視頻流像素緩沖區(qū):

Objective-C
 NSDictionary *options = @{
    (id) kCVPixelBufferCGImageCompatibilityKey : 
        @YES,
    (id) kCVPixelBufferCGBitmapContextCompatibilityKey : 
        @YES,
 };

這個(gè)例子中鍵(key)默認(rèn)為 (id) 類型, 并且使用 Objective-C 字面量的方式將布爾類型的值轉(zhuǎn)換為 NSNumber 類型。Swift 版本的方式將會(huì)更加得簡(jiǎn)便。而且編譯器已經(jīng)足夠智能去把字典中值和類型關(guān)聯(lián)起來(lái)(譯者注:類似腳本語(yǔ)言,賦予值后就會(huì)自動(dòng)聲明為該值的類型)。

let myOptions: [NSString: NSObject] = [
    kCVPixelBufferCGImageCompatibilityKey: true,
    kCVPixelBufferCGBitmapContextCompatibilityKey: true
]

即使在 Swift 中,像這樣將值傳遞給 API 也不是一種很理想的方式。

配置字典的特征

下面的例子展示了配置字典(Setting dictionary)的共同特征,這都是值得仔細(xì)研究的。

  • 它們有一組固定的鍵(key),大概有 12 個(gè)這樣的像素緩沖區(qū)屬性鍵在 AVFoundation 庫(kù)里。這個(gè)集合里面的鍵很少會(huì)被改變,并且和一個(gè)已經(jīng)確立的功能有關(guān)。

  • 實(shí)例中所對(duì)應(yīng)鍵的值(value)均是特定的已知類型。這些類型相比 NSObject 類型來(lái)說(shuō)顯得更加細(xì)致,例如描述「圖像右側(cè)內(nèi)邊距的像素大?。?code>CFNumber 類型)」。

  • 類型的安全關(guān)系著值的傳遞,我們無(wú)法通過(guò)一個(gè) NSDictionary 保證值類型的正確傳遞。前面的例子因?yàn)榭紤]了兼容性,所以鍵的類型應(yīng)該是 Boolean 類型,而不是 IntStringArray,甚至 NSNumber 類型。

  • 有效的條目在字典中只會(huì)出現(xiàn)一次,因?yàn)殒I通過(guò)哈希散列存儲(chǔ),新條目將會(huì)覆蓋舊條目。

在 Swift 中,上述列出的特性在集合和枚舉中顯得比字典更加典型。理由如下:

  • 枚舉列出了所有給定類型的可能選項(xiàng)。就類似于這個(gè)例子一樣,大多數(shù) Cocoa API 都有固定、不變的鍵。

  • 在個(gè)別情況下,枚舉可以關(guān)聯(lián)類型值。Cocoa API 文檔記錄下了每個(gè)鍵可以傳遞的值類型。

  • 像字典一樣,集合中有成員的限制以避免多個(gè)實(shí)例的產(chǎn)生。

基于這些原因,我覺(jué)得在 Swift 中設(shè)置集合時(shí)使用枚舉會(huì)比一個(gè) [NSString: NSObject] 的字典表達(dá)效果更好。

鍵的轉(zhuǎn)換

停下來(lái)思考一下我們現(xiàn)在碰到的這個(gè)狀態(tài)。AVFundation 定義了接下來(lái)所表示的一系列的鍵(順便,這不是一個(gè)完整的包含所有像素緩沖鍵值的集合)。

const CFStringRef kCVPixelBufferPixelFormatTypeKey;
const CFStringRef kCVPixelBufferExtendedPixelsTopKey;
const CFStringRef kCVPixelBufferExtendedPixelsRightKey;
const CFStringRef kCVPixelBufferCGBitmapContextCompatibilityKey;
const CFStringRef kCVPixelBufferCGImageCompatibilityKey;
const CFStringRef

上面都是一些常量字符串,這些字符串都被用于作為字典的索引。調(diào)用者通過(guò)使用這些鍵來(lái)創(chuàng)建字典,通過(guò)傳遞任意對(duì)象作為鍵的值。

在 Swift 中,你可以將這個(gè)基于鍵值對(duì)存放的數(shù)據(jù)類型改造成一個(gè)簡(jiǎn)單的枚舉:對(duì)于不同鍵所表示的情況,指定特定的值類型。下面例子就是用來(lái)表示上面的五種情況。所關(guān)聯(lián)的類型來(lái)自現(xiàn)有的像素緩沖鍵值屬性文檔。

enum CVPixelBufferOptions {
 case CGImageCompatibility(Bool)
 case CGBitmapContextCompatibility(Bool)
 case ExtendedPixelsRight(Int)
 case ExtendedPixelsBottom(Int)
 case PixelFormatTypes([PixelFormatType])
 // ... etc ...
}

當(dāng)這些選項(xiàng)像這樣設(shè)計(jì)時(shí),我們就得到了一個(gè)可擴(kuò)展性很強(qiáng)而又對(duì)每種可能情況嚴(yán)格規(guī)定值類型的枚舉類型。為一個(gè)可擴(kuò)展的枚舉在每個(gè)可能的情況下,嚴(yán)格規(guī)定值的類型。和弱字典類型相比,這種方法能夠保證類型安全。

此外,在個(gè)別枚舉案例也會(huì)更清晰,更簡(jiǎn)潔,使用作為數(shù)據(jù)交互也比名字很長(zhǎng)很詳細(xì)的 Cocoa 形式的常量更好。例如 kCVPixelBufferCGBitmapContextCompatibilityKey 這個(gè)常量名字就顯得非常啰嗦。Cocoa 形式的常量通常會(huì)用 k 開(kāi)頭表示這是一個(gè)常數(shù),使用 CVPixelBuffer 表示相關(guān)聯(lián)的類,以及使用 key 表示其職責(zé),所有的內(nèi)容都在這里表示了。

創(chuàng)建配置集

通過(guò)重新設(shè)計(jì),你可以建立一個(gè)看上去應(yīng)該就像下面例子一樣的集合。說(shuō)「應(yīng)該」是因?yàn)檫@段代碼不能通過(guò)編譯。

// This does not compile yet
let bufferOptions: Set<CVPixelBufferOptions> = 
    [.CGImageCompatibility(true), 
     .CGBitmapContextCompatibility(true)]

Swift 不能編譯以上的代碼,因?yàn)?CVPixelBufferOptions 中的配置內(nèi)容(即 option)還未遵循 Hashable 協(xié)議。為了解決這個(gè)問(wèn)題,你可以建立一個(gè)數(shù)組,但是需要注意的是這個(gè)數(shù)組無(wú)法保證只有唯一成員屬性的條件:

// This compiles
let bufferOptions: [CVPixelBufferOptions] =
    [.CGImageCompatibility(true),
     .CGBitmapContextCompatibility(true)]

數(shù)組使用起來(lái)是十分友好的,但它無(wú)法保證配置元素的單一獨(dú)立特性。字典能夠提供這一點(diǎn),與此同時(shí)也正在推動(dòng)重新設(shè)計(jì)數(shù)組。

區(qū)分值

Hashable 協(xié)議使 Swift 可以區(qū)分不同的實(shí)例。集合和字典都使用了哈希來(lái)確保成員和鍵都是唯一的。如果沒(méi)有哈希,他們不能提供這些保證。

當(dāng)創(chuàng)建配置集合時(shí),你希望創(chuàng)建并不像下面例子這樣的集合,因?yàn)樵谶@個(gè)例子中同時(shí)存在多個(gè)帶有沖突的配置成員:

[.CGImageCompatibility(true),
 .CGImageCompatibility(false)] // which one?!

由于有不同的關(guān)聯(lián)值,這顯然是兩個(gè)不同的枚舉實(shí)例。在這個(gè)例子中,你會(huì)想讓集合丟棄除了第一個(gè)被添加到集合的元素的內(nèi)容,即僅僅留下 true 值。(字典遵循相反的規(guī)則。字典是替換現(xiàn)有成員,而不是丟棄。)

通過(guò)實(shí)現(xiàn)哈希,使你能夠比較枚舉類型。

實(shí)現(xiàn)哈希值

對(duì)于這個(gè)特定的用例,你需要?jiǎng)?chuàng)建一個(gè)哈希函數(shù),該函數(shù)只考慮唯一的情況,而不是考慮關(guān)聯(lián)值。目前在 Swift 中沒(méi)有提供此功能的構(gòu)造函數(shù),所以你需要自己創(chuàng)建這個(gè)構(gòu)造函數(shù)。

Swift 中 Hashable 要遵從 Equatable 協(xié)議,因此,你的實(shí)現(xiàn)必須解決兩組的要求。對(duì)于 Hashable 協(xié)議,你必須返回一個(gè)哈希值。對(duì)于 Equatable 協(xié)議 ,必須實(shí)現(xiàn) == 函數(shù)。

public var hashValue: Int { get } // hashable
public func ==(lhs: Self, rhs: Self) -> Bool // equatable

基本的枚舉,例如 MyEnum {case A, B, C} 提供了原始值,這個(gè)原始值告訴你哪些項(xiàng)你正在使用。這些值都是從零開(kāi)始,并都使用起來(lái)十分方便。不幸的是,枚舉的關(guān)聯(lián)值不提供原始值的支持,使這項(xiàng)工作變得更加困難。所以,你必須親手建立哈希值。

下面是 CVPixelBufferOptions 的擴(kuò)展 ,它手動(dòng)為每一種情況增加哈希值。

extension CVPixelBufferOptions: 
    Hashable, Equatable {
    public var hashValue: Int {
        switch self {
          case .CGImageCompatibility: return 1
          case .CGBitmapContextCompatibility: return 2
          case .ExtendedPixelsRight: return 3
          case .ExtendedPixelsBottom: return 4
          case .PixelFormatTypes: return 5
       }
    }
}

public func ==(lhs: CVPixelBufferOptions,
    rhs: CVPixelBufferOptions) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

從最直觀的一面可以看出這些哈希值絕對(duì)沒(méi)有任何意義而且也不會(huì)暴露給 API 的使用者,所以如果你需要添加額外的值,你也可以這樣做。這種做法是有些丑陋以及不那么的 Swift 化,但是很有技巧性。

一旦你添加這些功能,一切都將開(kāi)始工作。你可以創(chuàng)建配置集合,來(lái)保證每一個(gè)配置只出現(xiàn)一次以及保證它們的值關(guān)聯(lián)的是正確的類型。

最終的思考

在這篇文章中所描述的稍些笨重的哈希方法在使用上遠(yuǎn)勝于 Cocoa 提供的 NSDictionary 方法。類型安全、枚舉和集合為那些古老過(guò)時(shí)的 API 提供了更好的解決方案。

Swift 真正所需要的,我想是配置集合與關(guān)聯(lián)值能更好的關(guān)聯(lián)在一起。至少,在所有的枚舉項(xiàng)中添加原始值的支持(不只是基本的那些缺乏相關(guān)的或內(nèi)在價(jià)值)將是向前邁進(jìn)一大步。

感謝 Erik Little。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問(wèn) 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)容

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