【译】Swift 中集合与字典的角逐

作者:Erica Sadun,原文链接,原文日期:2015-10-19
译者:CMB;校对:Cee;定稿:千叶知风

传统的 Cocoa 在使用字典时有个不好的习惯。无论是用户信息还是字体选项亦或是视频流(AVFundation)设置,NSDictionary 一直担任 Cocoa 传递数据的角色。字典是灵活的、易用的,但它也存在诸多潜在的危险。

在这篇文章中,我将讨论另一种更加 Swift 化的方法。这并不是一个能够彻底解决问题的方法,但我认为它是一个在后 Swift 时代中能够更好展示 API 是如何工作的观念模式。

基于字典的设置工作

下面的代码是从我自己的一个项目中抽取出来的(如果你熟悉我其他的文章或许会有印象,这是我写的 Movie Maker 类)。这几行 Objective-C 代码创建了一个名为 options 的字典,用来构建一个视频流像素缓冲区:

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

这个例子中键(key)默认为 (id) 类型, 并且使用 Objective-C 字面量的方式将布尔类型的值转换为 NSNumber 类型。Swift 版本的方式将会更加得简便。而且编译器已经足够智能去把字典中值和类型关联起来(译者注:类似脚本语言,赋予值后就会自动声明为该值的类型)。

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

即使在 Swift 中,像这样将值传递给 API 也不是一种很理想的方式。

配置字典的特征

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

  • 它们有一组固定的键(key),大概有 12 个这样的像素缓冲区属性键在 AVFoundation 库里。这个集合里面的键很少会被改变,并且和一个已经确立的功能有关。

  • 实例中所对应键的值(value)均是特定的已知类型。这些类型相比 NSObject 类型来说显得更加细致,例如描述「图像右侧内边距的像素大小(CFNumber 类型)」。

  • 类型的安全关系着值的传递,我们无法通过一个 NSDictionary 保证值类型的正确传递。前面的例子因为考虑了兼容性,所以键的类型应该是 Boolean 类型,而不是 IntStringArray,甚至 NSNumber 类型。

  • 有效的条目在字典中只会出现一次,因为键通过哈希散列存储,新条目将会覆盖旧条目。

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

  • 枚举列出了所有给定类型的可能选项。就类似于这个例子一样,大多数 Cocoa API 都有固定、不变的键。

  • 在个别情况下,枚举可以关联类型值。Cocoa API 文档记录下了每个键可以传递的值类型。

  • 像字典一样,集合中有成员的限制以避免多个实例的产生。

基于这些原因,我觉得在 Swift 中设置集合时使用枚举会比一个 [NSString: NSObject] 的字典表达效果更好。

键的转换

停下来思考一下我们现在碰到的这个状态。AVFundation 定义了接下来所表示的一系列的键(顺便,这不是一个完整的包含所有像素缓冲键值的集合)。

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

上面都是一些常量字符串,这些字符串都被用于作为字典的索引。调用者通过使用这些键来创建字典,通过传递任意对象作为键的值。

在 Swift 中,你可以将这个基于键值对存放的数据类型改造成一个简单的枚举:对于不同键所表示的情况,指定特定的值类型。下面例子就是用来表示上面的五种情况。所关联的类型来自现有的像素缓冲键值属性文档。

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

当这些选项像这样设计时,我们就得到了一个可扩展性很强而又对每种可能情况严格规定值类型的枚举类型。为一个可扩展的枚举在每个可能的情况下,严格规定值的类型。和弱字典类型相比,这种方法能够保证类型安全。

此外,在个别枚举案例也会更清晰,更简洁,使用作为数据交互也比名字很长很详细的 Cocoa 形式的常量更好。例如 kCVPixelBufferCGBitmapContextCompatibilityKey 这个常量名字就显得非常啰嗦。Cocoa 形式的常量通常会用 k 开头表示这是一个常数,使用 CVPixelBuffer 表示相关联的类,以及使用 key 表示其职责,所有的内容都在这里表示了。

创建配置集

通过重新设计,你可以建立一个看上去应该就像下面例子一样的集合。说「应该」是因为这段代码不能通过编译。

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

Swift 不能编译以上的代码,因为 CVPixelBufferOptions 中的配置内容(即 option)还未遵循 Hashable 协议。为了解决这个问题,你可以建立一个数组,但是需要注意的是这个数组无法保证只有唯一成员属性的条件:

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

数组使用起来是十分友好的,但它无法保证配置元素的单一独立特性。字典能够提供这一点,与此同时也正在推动重新设计数组。

区分值

Hashable 协议使 Swift 可以区分不同的实例。集合和字典都使用了哈希来确保成员和键都是唯一的。如果没有哈希,他们不能提供这些保证。

当创建配置集合时,你希望创建并不像下面例子这样的集合,因为在这个例子中同时存在多个带有冲突的配置成员:

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

由于有不同的关联值,这显然是两个不同的枚举实例。在这个例子中,你会想让集合丢弃除了第一个被添加到集合的元素的内容,即仅仅留下 true 值。(字典遵循相反的规则。字典是替换现有成员,而不是丢弃。)

通过实现哈希,使你能够比较枚举类型。

实现哈希值

对于这个特定的用例,你需要创建一个哈希函数,该函数只考虑唯一的情况,而不是考虑关联值。目前在 Swift 中没有提供此功能的构造函数,所以你需要自己创建这个构造函数。

Swift 中 Hashable 要遵从 Equatable 协议,因此,你的实现必须解决两组的要求。对于 Hashable 协议,你必须返回一个哈希值。对于 Equatable 协议 ,必须实现 == 函数。

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

基本的枚举,例如 MyEnum {case A, B, C} 提供了原始值,这个原始值告诉你哪些项你正在使用。这些值都是从零开始,并都使用起来十分方便。不幸的是,枚举的关联值不提供原始值的支持,使这项工作变得更加困难。所以,你必须亲手建立哈希值。

下面是 CVPixelBufferOptions 的扩展 ,它手动为每一种情况增加哈希值。

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
}

从最直观的一面可以看出这些哈希值绝对没有任何意义而且也不会暴露给 API 的使用者,所以如果你需要添加额外的值,你也可以这样做。这种做法是有些丑陋以及不那么的 Swift 化,但是很有技巧性。

一旦你添加这些功能,一切都将开始工作。你可以创建配置集合,来保证每一个配置只出现一次以及保证它们的值关联的是正确的类型。

最终的思考

在这篇文章中所描述的稍些笨重的哈希方法在使用上远胜于 Cocoa 提供的 NSDictionary 方法。类型安全、枚举和集合为那些古老过时的 API 提供了更好的解决方案。

Swift 真正所需要的,我想是配置集合与关联值能更好的关联在一起。至少,在所有的枚举项中添加原始值的支持(不只是基本的那些缺乏相关的或内在价值)将是向前迈进一大步。

感谢 Erik Little。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容