在 Swift 中使用 Objective-C 风格的异步 API

作者:Ole Begemann,原文链接,原文日期:2017-01-19
译者:Cwift;校对:walkingway;定稿:CMB

许多 Objective-C 风格的异步 API 会在它们的回调闭包中传入两个可选类型值:一个代表操作成功时方法的返回值,另一个代表操作失败时返回的错误值。

一个例子是 Core Location 框架中的 CLGeocoder.reverseGeocodeLocation 方法。它接受一个 CLLocation 对象,然后将坐标信息发送到 Web 服务器,服务器会将坐标解析为可读的地址。当网络请求完成时,该方法会调用回调闭包,参数为一个存储 CLPlacemark 对象的可选数组以及一个可选型的 Error 对象:

class CLGeocoder {
    ...
    func reverseGeocodeLocation(_ location: CLLocation,
        completionHandler: @escaping ([CLPlacemark]?, Error?) -> Void)
    ...
}

在 Objective-C 风格的 API 中,返回一对可选型的成功值和错误的模式是处理这种情况时最实用的方案。

两个可能的结果,四个潜在的状态

当前 API 的问题是,操作实际上只有两种可能:请求成功并返回结果,或者失败并返回错误。然而,这段代码却允许四种不同的状态:

  1. 结果非空,错误为空。
  2. 错误非空,结果为空。
  3. 二者都不为空。
  4. 二者都为空。

API 的文档可以明确排除最后两种情况,但作为用户,你永远都不能真正确保文档是正确的。

使用 Result 实现更优的设计

在 Swift 中你可能像这样设计同样的 API:

class CLGeocoder {
    ...
    func reverseGeocode(location: CLLocation,
        completion: @escaping (Result<[CLPlacemark]>) -> Void)
    ...
}

现在回调闭包中只接受一个(非可选型)参数,它的类型为 Result<...>Result 是一个枚举,与 Swift 中的 Optional 类型非常相似。唯一的区别是:它可以在失败时保存错误值,而 Optional 只有成功时的关联值:

enum Result<T> {
    case success(T)
    case failure(Error)
}

Result 目前还不是 Swift 标准库中的成员,但它可能会在将来被引入。在此之前,自己定义它也很简单,或者可以使用当前流行的 antitypical / Result 库。(注:这个库中的 Result 与我这里使用的类型略有不同:它使用强类型的错误,即它有第二个泛型参数表示错误的类型。)

使用这个虚构的新 API,编译器可以保证传递给回调闭包的参数只能有两个状态,即成功或失败。你不必担心两个值都存在或都不存在的情形。

一个把 (T?, Error?) 转换成 Result<T> 的构造器

然而我们不能修改苹果的 API,所以对回调闭包中参数固有的模糊性无能为力。我们能做的是包含一个将可选的成功值和可选错误转换为单个 Result 值的逻辑。我在代码中为 Result 定义了一个便捷构造器:

import Foundation // needed for NSError

extension Result {
    ///通过一个可选型的成功值与一个可选型的错误值
    ///初始化一个 Result 对象。 
    /// 以便把苹果的异步 API 返回的值转换为一个 Result。
    init(value: T?, error: Error?) {
        switch (value, error) {
        case (let v?, _):
            // 如果值是非空的忽略错误
            self = .success(v)
        case (nil, let e?):
            self = .failure(e)
        case (nil, nil):
            let error = NSError(domain: "ResultErrorDomain", code: 1,
                userInfo: [NSLocalizedDescriptionKey:
                    "Invalid input: value and error were both nil."])
            self = .failure(error)
        }
    }
}

当两个输入都为 nil(通常不应该发生)的情况下,创建一个自定义错误放入结果中。此处我使用了 NSError,不过你可以使用任何遵守了 Error 协议的类型。定义了这个构造器之后,我像下面这样使用地理编码器的 API:

let location = ...
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
    // 把参数转换为 Result
    let result = Result(value: placemarks, error: error)
    // 只对这里的 result 做操作
    switch result {
    case .success(let p): ...
    case .failure(let e): ...
    }
}

使用了额外的一行代码,将参数转换为一个 Result 类型的值,从那时起,我就不必再担心未处理的情况了。

2017 年 1 月 20 日的更新:Shawn Throop 建议优化我之前所述的 CLGeocoder 扩展中的代码。你的代码将只调用基于 Result 的方法,这个方法会在内部调用原始的 API 并负责类型的转换:

extension CLGeocoder {
    func reverseGeocode(location: CLLocation,
        completion: @escaping (Result<[CLPlacemark]>) -> Void) {
        reverseGeocodeLocation(location) { placemarks, error in
            completion(Result(value: placemarks, error: error))
        }
    }
}

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

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

推荐阅读更多精彩内容