Swift:类型转换

作者:Andyy Hope,原文链接,原文日期:2016-08-17
译者:Cwift;校对:冬瓜;定稿:CMB

每隔一段时间,你都会遇到一些像独角兽一般前沿的情况,迫使你挑战你在当前的时代与领域内所积累的一切知识。而就在刚才我成为了这种情况的受害者。

在汉语中,“危机”一词由两个字符组成,
一个代表危险,另一个代表机会。
— 约翰·肯尼迪

援引自五十年代末最知名的美国人之一,三十五年后另一个美国人延续了这个话题:

Crisi-tunity!
— 荷马·辛普森

(译者注:分别截取了英文单词 crisis(危险)的前半部分和 opportunity(机会)的后半部分)

危险

之前我有一个机会对我负责的应用中的一个 API 的响应进行回顾、重新建模以及代码重构,它接受一个 JSON 类型的数据结构作为参数,其中包含有一个数组类型,混合存储了不同类型的模型。混合存储的原因是这些模型要在一个 UITableView 中的同一个 section 中按照时间顺序展示。这就使得把数据保存在两个单独的数组中然后再组合的方案行不通。
为了简化这个问题,我模拟了一个假的但却更有趣的 API 响应来展示这个难题:

"characters" : [
    {
        type: "hero",
        name: "Jake",
        power: "Shapeshift"
    },
    {
        type: "hero",
        name: "Finn",
        power: "Grass sword"
    },
    {
        type: "princess",
        name: "Lumpy Space Princess",
        kingdom: "Lumpy Space"
    },
    {
        type: "civilian",
        name: "BMO"
    },
    {
        type: "princess",
        name: "Princess Bubblegum",
        kingdom: "Candy"
    }
]

如你所见,这些对象在某些方面是相似的,但是又包含了一些其他对象没有的属性。让我们来看看一些可能的解决方案。

类和继承

class Character {
    type: String
    name: String
}
class Hero : Character {
    power: String
}
class Princess : Character {
    kingdom: String
}
class Civilian : Character { 
}
...
struct Model {
    characters: [Character]
}

这是一个完全有效的方案,但是用起来会令人感到沮丧,因为每当我们需要访问那些模型特有的属性时,我们都不得不进行类型检查并且把对象转换成特定的类型。

// 类型检测
if model.characters[indexPath.row] is Hero {
    print(model.characters[indexPath.row].name)
}
// 类型检测及转换
if let hero = model.characters[indexPath.row] as? Hero {
    print(hero.power)
}

结构体与协议

protocol Character {
    var type: String { get set }
    var name: String { get set }
}
struct Hero : Character {
    power: String
}
struct Princess : Character {
    kingdom: String
}
struct Civilian : Character { 
}
...
struct Model {
    characters: [Character]
}

因为使用了结构体,所以可以从系统层面优化性能,但是使用结构体仍旧与使用超类 非常 相似。因为我们没有使用协议的任何优势,目前的情况确实也没有什么能利用上的,为了访问类型的特定属性我们仍需有条件地进行类型判断和类型转换。

// Type checking
if model.characters[indexPath.row] is Hero {
    print(model.characters[indexPath.row].name)
}
// Type checking and Typecasting
if let hero = model.characters[indexPath.row] as? Hero {
    print(hero.power)
}

类型转换

现在你可能认为我非常鄙视类型转换,但是我没有。只不过在 这种 情况中,类型转换有可能向我们的代码中引入意料之外的副作用。如果向 API 中引入新的类型呢?依照我们对响应的解析,它可能什么都不做,也可能做一些事情。当我们编写代码时,我们应该保证代码总是有意义的,因为 Swift的设计原则是安全性优于其他考虑。

{
    type: "king"
    name: "Ice King"
    power: "Frost"
}

机会

我们现在面对的情况是:常规的尝试最终得到了相同的结果,所以我们被迫在知识的集合之外进行思考,那么不妨让想法大胆一点或者转换一下……不同的思路
如何创建一个强类型的对象数组,并且访问它们的属性时不需要类型转换?

枚举

enum Character {
    case hero, princess, civilian
}

因为一个 switch 语句必须是完备的,所以使用枚举可以有效地清除代码中的副作用。不过只有枚举还不够,我们需要再进一步。

关联类型

enum Character {
    case hero(Hero) 
    case princess(Princess)
    case civilian(Civilian)
}
...
switch characters[indexPath.row] {
    case .hero(let hero):
        print(hero.power)
    case .princess(let princess):
        print(princess.kingdom)
    case .civilian(let civilian):
        print(civilian.name)
}

再见了,类型转换...你好,类型转换™®©!
现在我们消除了那些只有所有类型转换都失败时才会暴露的潜在问题,实施了严格的类型验证,这会引导我们写出有意料之中的代码。

原始值

enum Character : String { // Error
    case hero(Hero) 
    case princess(Princess)
    case civilian(Civilian)
}

你可能已经注意到了,上例中的 Character 枚举不能遵守 RawRepresentable 协议,这是因为一个枚举在拥有关联类型的同时不能再遵守 RawRepresentable 协议,两者是互斥的。

初始化

init

所以如果我们不能通过原始值初始化,我们只能定义自己的构造器,因为原本的原始值初始化方式全部是由 RawRepresentable 协议来完成的,它提供了一个构造器以及一个供我们访问的 rawValue

枚举类型

enum Character {
    private enum Type : String {
        case hero, princess, civilian
        static let key = "type"
    }
}

一个枚举,在另一个枚举内部...这是一个枚举嵌套。

我们需要更深入...
哇,嗯,好吧谢谢再见

(译者注:原文用了一个诙谐的组合词 kthxbai 表示 OK, thank you, goodbye)

在我们使用构造器之前必须预设类型,最好的办法是使用枚举,因为当我们尝试初始化一个对象时,如果出现传入的 rawValue 不匹配枚举的任何一个 case 的情况,我们需要让本次初始化过程失败。用我们的 JSON 数据格式为例,它有 key 关键字来校验类型,但是不必每次都使用这个关键字。在 JSON 对象中你只需要一个唯一的属性用来校验你想要建模的类型即可。

允许失败的构造器

如果 Type 枚举使用 rawValue 的初始化方式失败了,那么会引起 Character 对象的初始化失败。我们将为关联类型中的每一个成员定义一个相似的允许失败的构造器,因为 Swift 的编译器不允许我们使用一个没有值的枚举对象,除非该值被声明为可选型。

// 枚举字符
init?(json: [String : AnyObject]) {
    guard let 
        string = json[Type.key] as? String,
        type = Type(rawValue: string)
        else { return nil }
    switch type {
        case .hero:
            guard let hero = Hero(json: json) 
            else { return nil }
            
            self = .hero(hero)
 
        case .princess:
            guard let princess = Princess(json: json) 
            else { return nil }
            self = .princess(princess)      
        case .civilian:
            guard let civilian = Civilian(json: json) 
            else { return nil }
            self = .civilian(civilian)
    }
}

解析

// 初始化
if let characters = json["characters"] as? [[String : AnyObject]] {
    self.characters = characters.flatMap { Character(json: $0) }
}

当我们解析 JSON 数据的时候,因为 Character 枚举使用了允许失败的构造器,必须删除数组中的 nil 值,所以这里使用了 flatMap 方法,这样我们的数组就只包含那些非空的值了。

类型转换

switch model.characters[indexPath.row] {
    case .hero(let hero):
        print(hero.power)
    
    case .princess(let princess):
        print(princess.kingdom)
    
    case .civilian(let civilian):
        print(civilian.name)
}

现在你已经掌握这个技巧了,我们让自己的代码风格从意料之外的变成了意料之中的,并且在这个过程中学到了新的东西。在此之前,我们还不得不使用由 AnyAnyObject 或者其他一些通用的可继承或者可组合的模型所组成的数组,在使用这样的数组前需要进行类型检查和类型转换。

福利:使用模糊匹配进行类型转换

if case

假设我们有一个函数需要传入一个参数值,该参数是 Character 类型的,我们只关心在某一个特定的情况下的处理方案。在这种情况下使用 switch 可能被视为过度使用,因为相关的语法很多并且需要处理所有其他的情况:

func printPower(character: Character) {
    switch character {
        case .hero(let hero):
            print(hero.power)
        default: 
            break
}

我们可以使用模式匹配作为替代,并通过使用 ifguard 语句缩短我们的代码,使其更加简洁:

func printPower(character: Character) {
    if case .hero(let hero) = character {
        print(hero.power)
    }
}

像往常一样,我在 GitHub 上为你提供了一个 playgrounds,同时,如果你手边没有 Xcode 的话,请查阅这个 Gist
如果你喜欢今天阅读的内容,你可以查看我的其他文章,如果你打算在自己的项目中使用这种方式,请给我发一条 tweet,或者在 Twitter 上关注我,这会让我感到很开心。

另外,九月份的时候我会在 try! Swift NYC 上发言,到时候会有一帮非常了不起的家伙们到场。非常希望能在那看到你,如果你看到我,记得来打个招呼!

公告

我是一个在 Swift 方面比较沉闷的人,但是社区是一个精彩的地方,我真的想为社区的成长贡献力量。我已经决定 2 月 23、24 日在墨尔本举办一个会议,我称之为 Playgrounds。另外,我们有一个 CFP 开放到 11 月 4 日,所以下手要快了!

详情参见 www.PlaygroundsCon.com ,或者在 Twitter 上关注我们:@PlaygroundsCon

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

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

推荐阅读更多精彩内容