Futures/Promises 概览:我是如何爱上 GCD 的
OliSwope
8年前
<p>这是一篇关于 Swift 中的 Futures/Promises 架构概览,演讲者为我们着重介绍了 FutureKit 的使用方式,从而避免再去调用恼人的 dispatch_async 。同时这也是一篇关于异步处理的简要介绍,演讲者讲述了 Futures/Promises 框架是如何帮助我们来实现异步处理的。</p> <p>通过本次讲演,我们会了解到以下内容:</p> <ul> <li>如何将 Promise 和 Future 互相结合</li> <li>如何使用 executor 从而让 GCD 更简单</li> <li>不用再编写带有回调闭包属性的函数了</li> <li>如何将您喜爱的异步库封装到 Future 当中(例如 Alamofire、ReactiveCocoa 等等)</li> <li>创建稳定的 (rock solid) 错误处理,以及异常简单的取消操作 (cancellation)</li> </ul> <p>我同样也会对诸如缓存 (caching)、并发执行 (parallel execution)、 NSOperationQueues 、基于 FIFO 的运行之类的编码模式,来对 Futures 做一个简要的概述。</p> <p>今天我想要谈论的是 <strong>FutureKit</strong> 这个框架。它不仅仅是 Futures-Promises 架构的 Swift 实现,并且我觉得它与其他解决方案相比要更为 “Swift 化”。我们将会谈论一下 Future、Promise 等这些术语究竟是什么意思,以及为什么实际上没有人能完全遵守这些观念。</p> <p>格的不同而已。至于您喜不喜欢这个解决方案,这完全取决于您自己,但是您至少应该了解一下这些解决方案的不同之处。</p> <p>在我们讨论 Futures/Promises 的概念之前,我们需要先讨论一下代码块 (Block) 和闭包 (Closure)。</p> <h3><strong>代码块与闭包</strong></h3> <p>代码块与闭包的好处都有啥?它可以让您摆脱那些恼人的委托协议 (delegate protocol)。尽管有些委托协议还是没法摆脱,但是要注意的是,您可以只定义一个带有回调闭包 (callback block) 的函数。比如说在 GCD (Grand Central Dispatcher) 当中,如果您想要在非主线程当中执行某些操作的话,那么您就需要闭包的帮忙。</p> <p>现在我们要讨论一下这么做的缺点。</p> <pre> <code class="language-javascript">func asyncFunction(callback:(Result) -> Void) -> Void { dispatch_async(queue) { let x = Result("Hello!") callback(x) } }</code></pre> <p>这是一个很常见的例子了。我封装了一个 asyncFunction 方法,从中我创建了某种 Result,但是如果我想要将这个 Result 回调的话,那么我就必须要提供一个回调例程(callback routine)。</p> <p>如果您经常处理异步事务的话,那么这种代码您此前也一定写过。这里是我所编写的一个例子,无论如何,我们都要进入到一个后台线程 (background queue) 当中,然后在那里生成我们所要的 Result,接着将 Result 回调回去。</p> <h3><strong>回调闭包的问题所在</strong></h3> <p>事实上,我将会讨论一下 <strong>AFNetworking</strong> 的相关设计。我觉得之所以 AFNetworking 能够成为 iOS 开发者首选的第三方库,是因为它摈弃了存在于 NSURL 当中的那些可怕的委托协议。它是基于回调进行的。</p> <pre> <code class="language-javascript">Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]) .response { request, response, data, error in print(request) print(response) print(data) print(error) }</code></pre> <p>这是 Alamofire 的一个例子,是它最新版本的一个示例。我在这里不会讲解 Alamofire 的相关知识。虽然我不喜欢 Alamofire 这个第三方库,但是我觉得以它为例来介绍回调闭包是再好不过的了,我们提出了一个网络请求以获取相关数据。这里我将会获取到某种网络回应 (response) 对象,并且您可以看到这些选项—— request 、 response 、 data 以及 error ——这些选项都是可能在这个异步 API 调用的时候出现的。</p> <p>好的,大家可能会问了,这看起来挺正常的啊,怎么会有问题呢?实则不然,当您运行了某个带有回调闭包的函数时,就会出现很诡异的现象。举个例子,这个闭包将在何处运行呢?</p> <p>一旦您开始编写异步代码,这就意味着这段代码将不能在主线程当中运行,因为如果您将这段代码写到了主线程上,那么您的应用会卡顿在那儿,因此您必须要将这些代码放到后台来执行。因此,当您运行一个本身就在后台运行的回调函数时,按正常道理来说它随后会将结果回调给您,但是问题来了,这个回调的结果 <em>在哪里</em> 呢?这个回调闭包现在是否必须要派发 (dispatch) 到主线程上呢?但是如果这个回调我仍然不希望它在主线程上运行呢?</p> <p>另外的问题是,我们该如何进行错误处理呢?我是不是要实现两个回调?我看到过有 API 提供了一个成功时调用的回调代码,以及一个失败时调用的回调代码。我用一个例子给大家演示一下,为什么绝大多数人会使用 Swift 的枚举来规避这个情况的发生,当然如果您这么做也是很好的一个解决方案。但是这仍然存在一个问题,您该如何界定错误的范围呢?</p> <p>此外就是取消操作 (cancellation) 了。多年以来,对于这些第三方库来说有这么一个经久未决的问题:那就是如果我有一个操作,然后我取消了这个操作,那么是否需要调用取消的回调呢?以及,我该如何处理这个取消操作呢?</p> <pre> <code class="language-javascript">func getMyDataObject(url: NSURL,callback:(object:MyCoreDataObject?,error: ErrorType) -> Void) { Alamofire.request(.GET, url, parameters: ["foo": "bar"]).responseJSON { response in switch response.result { case .Success: managedContext.performBlock { let myObject = MyCoreDataObject(entity: entity!, insertIntoManagedObjectContext: managedContext) do { try myObject.addData(response) try managedContext.save() dispatch_async(dispatch_get_main_queue(), { callback(object: myObject, error: nil) }) } catch let error { callback(object: nil,error: error) } } case .Failure(let error): callback(object: nil,error: error) } } }</code></pre> <p>这是一个典型的获取数据的函数,用标准的 GCD 回调写成。我执行了 Alamofire 网络请求,准备去 GET 数据。然后我得到了相应的网络回应,它是以枚举的形式返回,这是一个很好的范例。</p> <p>再提及一遍,之所以给大家展示 Alamofire 的原因不是因为我喜欢它,而是我觉得它在使用回调方面无出其右,但是我们还是来看一下它的缺点所在。</p> <p>现在我们获取到了 response 对象。如果成功的话,呃,这是一个 API 调用,然后我想要从服务器那里返回一个模型对象。我们使用这个 URL 来进行 Alamofire 的网络访问,假定我得到的网络回应是成功的。</p> <p>这时候,我会创建一个相应的管理对象上下文 (managed object context),然后对其执行 performBlock ,由于管理对象上下文处于不同的上下文当中,并且我还要确保我没有在主线程执行这个方法。因此我需要使用后台进程。</p> <p>这时候让我们去调用这个 performBlock 。现在我位于另一个线程当中了,所以也就是说,我现在是很安全的,这个时候我们就来构造 MyCoreDataObject ,然后使用这个 addData 方法,这个方法是我自行编写的,它可能会抛出一个错误。</p> <p>这里我们就要借助 Swift 的错误处理机制来实现错误处理了,因为可能我访问的服务器并没有返回合法的 JSON 给我。之后就是尝试将更改操作保存到 Core Data 里面,这个操作也可能会抛出错误。</p> <p>最后,由于我知道当前我正位于后台线程当中,因此我不希望在 Core Data 的队列当中返回我想要的结果,因此我需要在主线程执行这个回调。</p> <p>通过这样,将使得其他人在调用这个 API 时不会犯太多的错。它们可以直接在回调中修改界面,以及执行其他适合在主线程当中完成的事情。因此我会在 dispatch_async 当中完成这段操作。如果发生了错误,那么我就必须要返回一个错误类型。</p> <p>实际上,您可以很清楚地看到,在这其中出现了三次嵌套 (interaction) 的回调。这些回调甚至还有可能会嵌套到五次、六次,而这则会导致更糟糕的事情发生。</p> <h3><strong>为什么要使用 Futures/Promises</strong></h3> <p>那么什么是 Futures/Promises 呢,为什么人们要使用它们呢?基本上,它们可以帮您摆脱回调的困扰。</p> <p>我们使用过大量的 JavaScript Promises,它们用起来真的很棒。那么 Future 和 Promise 有什么区别呢?毕竟它们都是用来让您摆脱回调的困扰的,这是它们最主要的作用。</p> <p>那么这两个词到底什么意思呢,我该什么时候用 Future,什么时候用 Promise 呢?在我见过任何一种实现中,这两者都不一致。JavaScript 只是将事件称之为 Promise,但是 JavaScript 是动态类型的,所以使用起来比较单。此外也有只使用 Future 的相关实现,因为在理论中,所有的操作都可以在一个单独的对象当中完成。</p> <p>我从 Scala 那里窃取来了 FutureKit 的实现。再次强调一点,这只是一种选择,因为这些实现方案基本上是非常相似的。对于 FutureKit 来说,我们在用户界面当中需要使用 Future 对象。这意味着您的函数很可能需要返回一个 Future 对象。</p> <p>如果您正在定义诸如函数之类的东西,那么您所提供给用户的 API 需要返回一个 Future 对象,而 Promise 则更为底层,它是用来生成 Future 的东西。我喜欢 Promise 这个词语,因为当您创建一个 Promise 的时候,您必须要信守诺言 (keep promise)。如果您创建了一个 Promise,并返回了一个 Future,那么您就必须要完成这个操作,否则的话您的代码就会异常终止。</p> <pre> <code class="language-javascript">func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> { let executor = Executor.ManagedObjectContext(managedContext) Alamofire.request(.GET, url, parameters: ["foo": "bar"]) .FutureJSONObject() .onSuccess(executor) { response -> MyCoreDataObject in let myObject = MyCoreDataObject(entity: entity!, insertIntoManagedObjectContext: managedContext) try myObject.addData(response) try managedContext.save() return MyCoreDataObject } } }</code></pre> <p>这段代码和我们之前所看到的那一段代码在功能上是一样的。不同的是,这是用 FutureKit 所实现的。</p> <p>首先,您会看到顶部的函数结构非常不一样,现在回调已经不存在了。它现在只是,接收一个我需要获取数据的 URL 地址,然后返回一个 Future<MyCoreDataObject> 。注意到 MyCoreDataObject 的可选值已经不存在了。</p> <p>随后我在这里创建了一个 executor 。这是另一个我基于 ManagedObjectContext 所创建的 FutureKit 产物。如果您使用过 Core Data 的话,就会明白,在这里它为您封装好了 performBlock 。</p> <p>好的,现在我执行了 Alamofire 的网络请求,这个 FutureJSONObject 是 FutureKit 对 Alamofire 的一个扩展,它可以将 response 对象从 Alamofire 的形式转换为 Future。</p> <p>现在我就可以说了, onSuccess(executor) 意味着,后面的这个闭包就是在 performBlock 当中要运行的那段代码。如果您注意到这里的架构的话,就会发现我可以获取到我所获取的 response 对象,然后返回 Core Data 对象即可。</p> <p>随后就是 try 操作了。注意到我们没有添加任何的 do 或者 catch 。这是因为这些错误处理已经由 handler 自行帮我们处理好了。因此现在我就可以直接使用 try 来添加数据、保存数据,然后将对象返回即可。</p> <p>这里的关键在于,这个函数要返回一个名为 Future 的对象,Future 当中拥有许多 handler 来处理里面的内容。</p> <h3><strong>最主要的 handler: onComplete</strong></h3> <p>在 FutureKit 当中最主要的 handler 当属 onComeplete 了。如果您切实了解了 onComplete handler,那么您就能明白 FutureKit 当中的其他 handler 是如何工作的了。您会看到,其他的 handler 都是基于这个 onComplete 的便利封装而已。</p> <pre> <code class="language-javascript">let newFuture = sampleFuture.onComplete { (result: FutureResult<Int>) -> Completion<String> in switch result { case let .Success(value): return .Success(String(value)) case let .Fail(err): return .Fail(err) case .Cancelled: return .Cancelled } }</code></pre> <p>我调用了 Future 对象的 onComplete 方法,然后得到了这个名为 FutureResult 的枚举对象。再次强调一下,这个对象是一个泛型,因此这里我们得到的一个包含某种整数值的 FutureResult,然后最后我们会返回一个 Completion 对象。现在我来具体说明一下这两个对象。</p> <pre> <code class="language-javascript">public enum FutureResult<T> { case Success(T) case Fail(ErrorType) case Cancelled }</code></pre> <p>首先第一个是 FutureResult 。这基本上是每个 Future 结束的时候都会生成的玩意儿。</p> <p>与其他 Future 的实现所不同的是,FutureKit 增加了一个名为 Cancelled 的枚举。我们来详细解释一下这个 Cancelled ,为什么要把它单独提取出来,而不是放到 Success 或者 Fail 当中,而这种做法往往是其他异步实现方案所做的。</p> <pre> <code class="language-javascript">public enum Completion<T> { case Success(T) case Fail(ErrorType) case Cancelled case CompleteUsing(Future<T>) }</code></pre> <p>这是另一个名为 Completion 的枚举。 Completion 似乎很容易让人困惑,但是实际上它很好理解。</p> <p>最简单的方式就是将 Completion 视为结束 Future 的一项操作。您可以将其视为另一个 Promise,这样它看起来就有点像是 FutureResult,但是它拥有了额外的枚举值:我希望先完成括号里面的 Promise,然后再来完成当前的这个 Promise。这样当我们开始组合 Promise 的时候,它就非常有用了。在这个 onComplete 当中,您实际上可以看到这样的做法。</p> <pre> <code class="language-javascript">case let .Success(value): return .Success(String(value)) case let .Fail(err): return .Fail(err)</code></pre> <p>这里实际上有两个不同的枚举。第一个是 FutureResult,第二个是 Completion。</p> <p>绝大多数时候,您都不会去调用 onComplete 。您只需要在有特殊情况的时候才去调用它。大多数人基本上都只会去调用 onSuccess 。</p> <pre> <code class="language-javascript">let asyncFuture5 = Future(.Background) { () -> Int in return 5 } let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in let five = value print("\(five)") return five }</code></pre> <p>这是一个很典型的例子,它展示了在 FutureKit 当中我们该如何使用这个 onSuccess 。首先我在第一行创建了一个 Future 对象。这是创建 Future 最轻松、最简单的方法了。这里使用了 .Background ,随后我们会对其进行更深的讲解,这是一个很有意思的 executor。</p> <p>我这里的做法是在后台当中创建一个 Future 对象,我们这里所做的就是生成这个数字 5。假定出于某种特殊的原因,执行这项操作会占用大量的时间,因此我希望在后台完成这项操作,所以我使用这种方法来生成 Future 对象。它实际上会给我们返回一个数字 5,好的现在让我们来看看这个 onSuccess 。</p> <p>现在我可以说,我需要确保 onSuccess 在主线程上运行,因为我需要执行某种 I/O 操作。我获取 Future 所返回的值,然后将其打印出来。好的现在这个时候, value 实际上是一个值类型,而不是一个枚举了,也就是说,它已经是我们所期望的值了。</p> <pre> <code class="language-javascript">let stringFuture = Future<String>(success: "5") stringFuture .onSuccess { (stringResult:String) -> Int in let i = Int(stringResult)! return i } .map { intResult -> [Int] in let array = [intResult] return array } .onSuccess { arrayResult -> Int? in return arrayResult.first! }</code></pre> <p>Future 不仅能够执行 onSuccess ,而且还可以将其映射为一个新的 Future 对象。</p> <p>假设我创建了这样一个 stringFuture ,我是从一个函数当中所获取到的。这里我们用了一个简便的方法,来创建一个已完成的 Future 对象。这个 Future 对象会返回一个字符串,并且它已经成功结束了。</p> <p>接着,在第一个闭包当中,我使用了 onSuccess ,我需要将字符串转换为 Int 类型。Swift 编译器的类型推断非常好用,它会自行知道下个 map 当中实际上会是什么类型,当然您也可以将 onSuccess 称之为 map ,因为这两者的行为非常相似。</p> <p>现在我会将我所得到的结果映射为 Int 数组。基本上,您可以将任何一种 Future 转换为另一种 Future。您会注意到我们这里的语法非常简明,如果您写过很多函数式和反应式代码的话,那么这种风格您一定不会陌生。因为这些风格非常相似,虽然有所不同,但是都非常简洁、美观、没有任何回调。</p> <pre> <code class="language-javascript">func coolFunctionThatAddsOneInBackground(num : Int) -> Future<Int> { // let's dispatch this to the low priority background queue return Future(.Background) { () -> Int in let ret = num + 1 return ret } } let stringFuture = Future<Int>(success: 5) stringFuture .onSuccess { intResult -> Future<Int> in return coolFunctionThatAddsOneInBackground(intResult) } .onSuccess { return coolFunctionThatAddsOneInBackground($0) }</code></pre> <p>这就是 Future 所带来的好处了,它可以使得用户界面变得高度可组合 (highly-composable)。这里我给大家展示一个稍微详细一点的示例。</p> <p>我创建了这个可以在后台执行数字运算的函数。这个函数需要在后台运行,然后将传递进去的数字加一,至于这样做的理由,是出于某种原因我们不希望在主线程运行它。现在,如果您注意到的话,我创建了这个 stringFuture ,这个 stringFuture 和之前的相同,但是我要做的是返回一个新的 Future,而不是返回一个新的数值。因此我们可以使用 map ,将这个值映射到另一个 Future。</p> <p>Futures/Promises 让代码变得易于组合。所有的 Future、Promise 的实现基本上都是由这些基本结构所组成的。这个特性非常讨人喜欢。</p> <p>那么什么是 Promise 呢?假设您需要在某个地方去创建一个 Future 对象。我创建了一个 Promise,这个 Promise 会返回一个字符串数组。随后,当我调用 completeWithSuccess 之后,我就可以得到这个真正的字符串数组了。</p> <p>```swift</p> <p>let namesPromise = Promise<[String]>()</p> <p>let names = [“Skyler”,”David”,”Jess”]</p> <p>namesPromise.completeWithSuccess(names)</p> <p>let namesFuture :Future<[String]> = namesPromise.Future</p> <p>namesFuture.onSuccess(.Main) { (names : [String]) -> Void in</p> <p>for name in names {</p> <p>print(“Happy Future Day (name)!”)</p> <p>}</p> <p>}</p> <pre> <code class="language-javascript">这意思是说,我实现了这个 Promise,如果 Promise 允诺了,那么我们就得到了结果。您会注意到,所有的 Promise 都包含这样一个名为 `.Future` 的成员。这就是您可以返回的 Future 对象,这是用来让 Promise 操作用户界面的方式之一。这样我现在就可以来实际执行这个 Promise。 ```swift func getCoolCatPic(catUrl: NSURL) -> Future<UIImage> { let catPicturePromise = Promise<UIImage>() let task = NSURLSession.sharedSession().dataTaskWithURL(catUrl) { (data, response, error) -> Void in if let e = error { catPicturePromise.completeWithFail(e) } else { if let d = data, image = UIImage(data: d) { catPicturePromise.completeWithSuccess(image) } else { catPicturePromise.completeWithErrorMessage("didn't understand response from \ (response)") } } } task.resume() // return the Promise's Future. return catPicturePromise.Future }</code></pre> <p>通常情况下,当您需要 Promise 的时候,这就意味着您需要封装某种接收回调为参数的东西。如果您遇到了某个使用了回调的第三方库的话,那么你可以借助 Promise 将它们 Future 化,以将它们转换成 Future 的形式。</p> <p>这里我举了一个很典型的例子,我是从 FutureKit 当中的 Playground 取出来的例子,这段代码的作用是获取一些可爱的猫咪照片。在幻灯片里面效果不是很好,因为在 Playground 当中,您可以切实看到猫咪的图片,但是这里你只能想象这段函数能够获取到猫咪的图片。</p> <p>我需要这样一个函数,给定 URL,然后返回图片给我。仔细想一下,为了获取这个图片对象,我不仅需要访问网络,从我的网络层那里将数据对象拉取下来,还需要将这个数据对象转换为 UIImage ,然后再将其返回。所有的这些步骤都已经封装在这个函数里面了。</p> <p>我将准备使用标准的 NSURLSession 。它接收我的 URL 为参数,然后现在我完成了这个回调 response 的设置。我从中拿取到了我需要的 data、response 和 error 对象,然后我就可以开始对其进行解析了。</p> <p>如果我接收到了错误,那么我就需要让我的 Promise 以失败告终。如果我接收到了 data,并且还可以将其转换成为图片,那么我的 Promise 就是成功的。如果这个操作失败了,并且我也不想创建一个自定义的错误类型的话,那么我就使用这个内置的错误方法,提示“我无法识别这个 response”。我们以这个 NSURLSession 开始,然后以 Future 结束。</p> <p>如果您对它运行的方式有疑问的话,其实这整个内部的闭包随后才会运行,但是这个 Future 会被立即返回。您可以知道这个 Future 当中封装了哪些内容。</p> <h3><strong>如何进行错误处理呢?</strong></h3> <p>现在让我们来看一下错误和错误处理。这正是我觉得 Future 的妙处所在,因为这样只要您的步骤正确,那么您就不用考虑潜在的问题了,Future 的错误处理非常好用。</p> <p>当我们在处理回调的时候,对于每个封装的单独 API 回调来说,我们都必须要处理错误情况。您必须要大量地检查错误。我不喜欢在回调中大量添加错误处理,因为它们会让回调变得更加复杂、更加糟糕。</p> <p>例如,假设您准备调用某个 API。然后突然发生了网络问题。假设我正从服务器获取一个 JSON 对象,因此有可能是我的 JSON 解析发生了错误,也有可能是验证出现了问题。好的现在我要对这些问题进行处理了,一旦我成功对对象进行了解析,那么我就需要将其存储在数据库当中。然后又发生了文件 I/O 的问题,或者也有可能是数据库内部数据验证的问题。</p> <p>这些问题都很有可能会发生。我希望能够给调用者提供一个高度封装的 API,它只执行某一件事情,而其他的错误则不必去关心。在 Future 的世界里,这意味着如果您的操作一切正确,那么 Future 将会输出正确的结果。</p> <p>FutureKit 并不会让您去定义特定错误类型的 Future。当您尝试去创建特定错误类型的 Future 时,它实际上破坏了架构的可组合性 (composability),正如您前面所看到的那样。因为这样的话您就必须要将所有的组合进行过滤,不停地进行 map 转换以分别处理不同的错误情况。您可能会觉得这种做法挺好的,但实际上它使得代码变得更糟了。</p> <p>另一个关于 FutureKit 的是,由于它没有特定错误类型,因此您会注意到它同样也没有 ErrorType 。有人总会建议您去创建一个不会返回 Error 的 Future,但是我们发现,这种做法实际上还是破坏了 Future 的可组合性。对于异步来说,它的核心思想在于这个操作有可能不会成功。就算现在不会出错,那么将来的某一天还是可能会出错。那么这种类型的 Future 来说,它们只能在没有错误发生的条件下才能正常工作。</p> <p>那么让我们来看一下 FutureKit 是怎么做的,如果您创建了一条 Future 调用链,但是您忘记在末尾加上错误处理的话,那么您会得到一个编译警告。它会告诉您:“您调用了 Future,但是我没有发现您对错误有任何的处理。”</p> <p>关于 FutureKit 的另一个好处在于,您所看到的这些 handler: onComplete 、 onSuccess 、 onFail 。它们都内置了 catch 。因此您就不必再用 catch 或者 do 去包装这些方法了。如果您调用的 Swift 方法需要使用 try 的话,那么不必担心。直接加上 try 即可,Future 在内部已经自行帮您完成了基础的错误处理了。</p> <pre> <code class="language-javascript">func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> { let executor = Executor.ManagedObjectContext(managedContext) Alamofire.request(.GET, url) .FutureJSONObject() .onSuccess(executor) { response -> MyCoreDataObject in let myObject = MyCoreDataObject(entity: entity, insertIntoManagedObjectContext: managedContext) try myObject.addData(response) try managedContext.save() return MyCoreDataObject }.onFail { error in // alert user of error! } }</code></pre> <p>现在,您就可以看到这个好用的 onFail handler 了。我回到我之前的例子当中,然后添加了这个 onFail handler,因为如果我对老代码进行编译的话,那么我会得到一条编译警告。现在我加上这条之后,就没有任何副作用了。</p> <p>在 FutureKit 当中,还有一点和其他解决方案不同。 onFail handler 并不会去干涉错误。这和 JavaScript 不同,您会选择去调用 catch,因为这样您就不用去理会错误了。但是实际上 catch 仍会对错误有所干涉,也就是说如果您如果在函数中使用了 catch 的话,那么人们很有可能会忘记您实际上对这个错误添加了某些副作用,如果不对这个错误进行处理,那么程序就很可能会发生崩溃。</p> <p>FutureKit 强制要求需要对错误编写 handler,因为您写的函数是异步的,我们都知道,异步操作很有可能会失败。 onFail 的目的在于处理错误,而不是捕获错误。因此您没必要将错误进行传递;您必须要使用 FutureKit 中的 onComplete 。</p> <p>虽然这可能只是所谓的习惯问题,因为 onFail 实际上已经可以执行很多操作了,但是我对我手下的开发者们并不信任,它们不一定会对去写 onFail 。</p> <p>现在您知道,当您在使用 onComplete 的时候,有人可能会将 onComplete 的涵义混淆起来,例如将一个失败的 Future 转换为一个成功的 Future。这种情况只占十分之一。其余的时候,如果 Future 失败了,您可能只是希望能够将中间数据清理掉,然后通知用户即可。</p> <h3><strong>取消操作 (Cancellation)</strong></h3> <p>好的,另一个您会在 FutureKit 当中看到的重要东西就是取消操作了。这是每个异步操作都可以使用的操作。当您开始一个异步操作的时候,您或许会意识到这个操作的结果是不需要的。您不希望那些 handler 被运行,因为这些 handler 已经毫无意义了。举个例子,您打开了某个视图,然后开始从网络上提取需要的数据,然后接着您点击了返回,关闭了这个视图控制器。而这个时候这个异步操作仍然还在运行,这个时候我们需要将其清除掉。</p> <p>现在,我们可以添加 onCancel 操作了。当我需要知道某个异步操作有没有被取消的时候,我们通常使用 onComplete 来完成。FutureKit 的取消操作非常好用。如果您看过 Github 上的源码的话,您会发现虽然代码不是很长,但是要明白它实际的运行原理还是非常困难的。</p> <p>现在让我们看一下这个会返回 Future 的函数:</p> <pre> <code class="language-javascript">let f = asyncFunc1().onSuccess { return asyncFunc2() } let token = f.getCancelToken() token.cancel()</code></pre> <p>这个函数已经和一个 onSuccess 组合在了一起,它接着会返回第二个 Future。问题是,如果我在上面调用了 onCancel ,那么我是取消了 asyncFunc1 还是取消了 asyncFunc2 呢?</p> <p>实际上这两个函数都会被取消掉。它会先取消第一个函数,然后如果第一个已经完成了,那么您也不必担心,它会取消第二个函数。如果您需要实现取消操作的话,那么很简单。在 Promise 上有一个 handler,它可以标明需要一个可取消的 Future。当您被告知 Future 被取消之后,您就需要对相关内容进行清除操作了。</p> <pre> <code class="language-javascript">let p = Promise<Void>() p.onRequestCancel { (options) -> CancelRequestResponse<Void> in // start cancelling return .Continue }</code></pre> <p>这实际上是一个枚举的 response,您既可以声明您暂时还不想要取消 Future,因为需要等待清理操作完成,或者也可以直接取消 Future。</p> <pre> <code class="language-javascript">public extension AlamoFire.Request { public func Future<T: ResponseSerializerType>(responseSerializer s: T) -> Future<T.SerializedObject> { let p = Promise<T.SerializedObject>() p.onRequestCancel { _ in self.cancel() return .Continue } self.response(queue: nil, responseSerializer: s) { response -> Void in switch response.result { case let .Success(t): p.completeWithSuccess(t) case let .Failure(error): let e = error as NSError if (e.domain == NSURLErrorDomain) && (e.code == NSURLErrorCancelled) { p.completeWithCancel() } else { p.completeWithFail(error) } } } return p.Future } }</code></pre> <p>您可以在 handler 中同时完成两个操作。我回到之前我用 FutureKit 对 Alamofire 做的扩展封装示例当中。我在这里对 Alamofire 序列化对象进行操作。这个函数是个泛型函数,允许我将 Alamofire 序列化对象转换为 Future。</p> <p>第一个事情是为试图序列化的对象创建一个 Promise,然后为其添加取消 handler。</p> <p>如果我需要执行取消操作的话,那么我就会调用这个 self.cancel ,这是内置的 Alamofire 取消请求方法,然后如果需要继续执行的话。那么接下来您会看到在这里,我对错误结果进行了处理,如果我发现错误是用户取消操作的话,那么我就让其 completeWithCancel 。</p> <h3><strong>新的 Swift 反面模式 (anti-pattern)</strong></h3> <p>一旦您理解了 Future 的原理,那么当您再去看自己的代码时,就会意识到原来的代码已经变成了一种全新的反面教材。当您看到别人的代码时,您会坐立不安:“请一定不要继续这么下去了!“</p> <p>其中一个反面模式就是那些带有回调属性的函数。当您看到它们的时候,就意味着麻烦来临了。另一个反面模式就是,我注意到,当初学者开始使用 Future 的时候,他们通常都会创建带有可选值结果的 Future。一般而言,这是不必要的。我们一般情况下是不会需要可选值的存在的。</p> <p>因为在回调中,之所以回调方法需要接受可选值作为参数,是因为所得到的不一定是实际结果,也有可能得到错误。如果没法得到预期结果的话,那么就说明这个过程肯定是失败了,这才是正确的做法。</p> <p>还有一件非常重要的事,这可以让您写出优秀的 Future 实现,就是如果我需要能够运行在后台的操作的话,那么我们应该让函数自身来定义自己的运行环境上下文 (execution context)。</p> <p>因此,对于我的这个图像库而言,我不希望它会在主线程上运行,而且我也不用让调用 API 的人员来操心这件事。我只需要让函数自身前往到合适的线程上去运行即可。我们接下来会谈论一下这是如何工作的。</p> <h3><strong>Executors</strong></h3> <p>FutureKit 当中,另一块重要的部分就是 executor 了。在 Future 其他语言的实现当中,它的名字可能会被命名为 ExecutionContext 。这是受到了 Bolts Executors 的启发。</p> <p>FutureKit 的 executor 实际上是一个枚举。正如您所注意到的,FutureKit 非常喜欢使用枚举。FutureKit 当中的 Executor 类所有的枚举值都与内置的 GCD 建立了镜像关系。绝大多数时间,您都只是使用内置的 GCD。很快,我就收到了反映这些名字的一系列数字。</p> <pre> <code class="language-javascript">let asyncFuture5 = Future(.Background) { () -> Int in return 5 } let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in let five = value print("\(five)") return five }</code></pre> <p>这就是我觉得 FutureKit 设计得非常好的地方。我创建了一个 Future,然后我希望它能够在后台运行,然后我运行了它。也就是用这个 onSuccess 命令来执行。我可以在这里添加一个 executor。</p> <p>实际上,executor 的种类有很多种。不过有一些是比较特殊的。首先是 .Immediate executor。这个即时 executor 意味着,我不在乎这个代码块在何处运行。它可以在任何地方运行。通常而言这是最常用、最有效率的 executor 了。我们通常而言都会直接使用这个 executor。比如说我们这里接收一个整数为参数,然后将其转换成字符串,而它运行的地方我们并不在乎。因此我们不必重新调整线程。</p> <p>此外还有 .MainImmediate ,这意味着我希望代码块在主线程运行,但是如果代码已经在主线程上运行了,那么就不用对线程进行调整了。如果没有的话,那么就将线程调整到主线程。还有 .MainAsync ,它意味着代码始终需要异步派遣 (async dispatch)。比如说某个委托非常奇怪,您必须要让委托结束运行,但是你还需要这段代码能够运行。</p> <p>然后还有一些 executor,比如说您已经创建了自己的派遣队列,那么您可以将这个队列封装到一个 executor 当中。您也可以将 NSOperationQueue 封装到 executor 当中。如果您还需要处理闭包操作的话,您还可以将管理对象上下文 (managed object context) 封装到 executor 当中。</p> <p>然后,还有一些非常智能的 executor。我毕竟倾向于使用这些 executor,因为它们实际上将多个 executor 的行为整合在了一起。您可以在代码当中声明它们,然后它们会在运行时映射为实际需要的 executor。最好用的一个就是 .Current 了。如果您已经使用了一个 executor 了,那么它会接着使用这个 executor。因为事实证明,对于程序员来说,因为您已经使用了一个 executor 了,一般那么就没必要再去创建一个新的了。代码一旦在后台执行了,那么就说明这段代码会一直想留在后台运行。</p> <p>好的,如果您在某个 executor 当中封装了自己的执行操作,或者并没有给 FutureKit 定一个 executor 的话,那么它会自行去推断合适的 executor。是谁在调用这段代码呢?这段代码位于何种运行操作上下文当中?我会确保这个闭包会自行进入到运行操作上下文档中,即使内部的方法决定需要去后台执行,它也会一直如此。</p> <p>下一个 executor 就是 .Primary 了。所谓的 .Primary 就是指那种您没有告知 FutureKit 您正在使用的是何种 executor,也没有说明您需要何种 executor,这是 executor 的默认值。最后四个都是可配置的,它们都是某种 FutureKit 的操作。</p> <p>我倾向于使用的 executor 是这个 .Async ,这意味着,我需要前往后台的某处来运行这段代码,或许随后我会决定要不要为默认操作改变 QoS。</p> <p>最后就是这个 .Custom 了, .Custom 允许您构建属于自己的 executor。举个示例,假设我有这样一个所需要的操作,它需要在主线程当中运行,但是我可能需要在后台队列当中花点时间来处理这段操作,因为这个操作并不是很重要。</p> <p>我们来举个比较怪异的例子吧,我们可以创建一个 executor,它会在后台执行,然后等待某个事件完成之后,又重新回到主线程来执行。</p> <pre> <code class="language-javascript">let ex: Executor = .Background let f = ex.execute { let data = expensiveToComputeData() return data }</code></pre> <p>executor 同样也有很好用的执行方法,这也是一种生成 Future 的简单方式。您在这里可以注意到,我创建了一个在后台运行的 executor,然后我调用了它的 execute 方法。我调用了某个方法,这个方法用来获取数据,但是非常地耗时间,然后我需要将这个数据返回。此外,还有一些带有延迟和额外事件的 execute 方法。</p> <pre> <code class="language-javascript">let shiftFuture = model.getShift() let vehicleFuture = model.getVehicle() combineFutures(shiftFuture, vehicleFuture) .onSuccess { (shift,vehicle) -> Void in print("\(shift),\(vehicle)") }</code></pre> <p>接下来,我们要做的就是将 Future 联合起来。在这个例子当中,我们需要并发执行。当我们有一系列需要一起运行的操作时,我们并不希望让它们一个个地运行。我们想要让这些操作全部立刻执行,因为您可能实在进行某种配置操作。让我们来运行以下这段代码,没有理由我们必须要将它们序列化。</p> <p>combineFutures 是一个非常好用的、类型安全的函数,它接收两个 Future 为参数,然后会创建一个带有双元结果的新 Future。或许我有某种能够生成 Shift 模型对象或者 Vehicle 模型对象的模型,现在我将准备把这两个 Future 组合成一个单独的 Future。当两个操作都成功之后,它将会把结果返回给我。再强调一遍,只要任意一个 Future 失败了,那么整个 Future 集都将失败。</p> <pre> <code class="language-javascript">public protocol CompletionType { associatedtype T var completion : Completion<T> { get } }</code></pre> <p>我想要再深入地谈一下 FutureKit 所做的东西。这个 CompletionType 协议是一个更高级的玩意儿。这是 FutureKit 的一个扩展,它可以让您自行建立想要的东西,它已经是异步化的了。</p> <p>或许您想要将 Alamofire 的网络请求改造成一个 Future,那么您可以使用这个协议对其进行扩展,然后将其转换成相应的类型,这样 FutureKit 就可以自行将其翻译,最后生成一个 Future。因此,这个新的请求就可以被加到 Future 的 handler 当中,这确实非常方便。</p> <pre> <code class="language-javascript">public protocol ErrorTypeMightBeCancellation : ErrorType { var isCancellation : Bool { get } }</code></pre> <p>这里有一个问题,我之前说过,取消操作并不是错误。但是如果您的某个库将取消操作视作错误的话,并且您还希望将这个错误转变为对应的取消操作,那么您可以使用这个 ErrorTypeMightBeCancellation 对错误进行扩展。之后您就可以计算 isCancellation 属性是否为真。只要为真,一旦这个错误在 handler 当中发生,那么 FutureKit 会自行将它转换为取消操作。</p> <h3><strong>进阶事项及结论</strong></h3> <p>既然您对这些基础知识已经有所了解了,那么我们下面就来聊一聊进阶事项吧!我不会去将这些第三方库的实现细节,但是我会给大家大概讲解一遍。</p> <pre> <code class="language-javascript">NSCache.findOrFetch(key, expireTime:NSDate? = nil, onFetch: () -> Future<T>)</code></pre> <p>为 Future 建立缓存是我一直想要做的事情。之所以这么做,是因为当您进行了一个时间耗费长的操作时,那么这项操作就必须异步进行,然后当该操作完成之后,您就可以将这个操作的结果缓存起来,这样就不用再次去执行这个操作了。比如说图像缓存之类的时间耗费长的操作就很适合进行缓存。</p> <p>但是缓存同样也会带来不好的问题,因为当这个异步操作开始的时候,只有当其成功结束之后才能够保存到缓存当中。当您用多了 Future 或者之类的异步后台运行代码的时候,您会发现很可能会有多个地方都需要执行这个操作,那么在第一个操作结束缓存之前,那么这些地方都仍将继续执行异步操作。</p> <p>我们可以通过一点小小的技巧来规避这个问题,这就要用到 NSCache 上的一个既有的扩展了。也就是 findOrFetch 函数,它允许您通过定义键来寻找缓存,如果查找失败,那么您就可以返回一个方法,这个方法返回一个 Future,接着 NSCache 就会去进行检索。接下来,如果其余的请求命中了这块缓存的话,那么它们所得到的都将是同一个 Future。它们都将获取同一个结果,这样您就可以加快这个异步请求的速度了。</p> <p>此外,还有一个名为 FutureBatch 的对象。当 combineFutures 力不从心的时候,尤其是您拿到了一个未知类型的 Future 数组并且也不知道里面有多少个 Future 的时候, FutureBatch 就派上用场了。此外,当其中的某个子 Future 失败的时候,如果您还需要进行很多控制和决定,比如说决定整个批次的 Future 是否都会全部失败,还是仅失败的那个子 Future 失败?如果您需要得到每个子 Future 的独立结果,那么请使用 FutureBatch 。</p> <p>此外还有 FutureFIFO ,麻雀虽小,五脏俱全。</p> <pre> <code class="language-javascript">let fifo: FutureFIFO() let f:Future<UIImage> = fifo.add { return functionThatBuildsUImage() }</code></pre> <p>另一件严肃的问题是,当您在撰写大量的异步代码时,您会意识到您需要进行序列化操作。 FutureFIFO 的特性是先进先出。它为您提供了一个 add 方法,您只需要重新返回一个 Future 即可。它确保这些要允许的代码块,在上一个代码块完成其 Future 之前,都不会运行。</p> <p>这和队列分割有所不同,队列分割会确保代码块一定会运行。这里是逻辑上的异步队列。最好的例子就是下面这个经典调用:</p> <p>首先,我要去与服务器通信。这时候我进行了某种 REST 操作,我要确保我能够执行 API 调用,并且还能够得到返回的 JSON 数据,之后我要去修改数据库,然后将数据写入到数据库当中,最后将结果返回,然后我就不想要进行下一个 API 调用了。我希望下次能够直接从数据库当中读取这个已写入好的数据。</p> <p>但是由于每次调用都位于不同的异步队列当中,这意味着所有的操作都会被执行。在这儿,您可以将其推入到 FIFO 队列当中,这样在前一个操作完成之前,后面的模型操作都不会进行。</p> <pre> <code class="language-javascript">NSOperationQueue.add<T>(block: () throws -> (Future<T>)) -> Future<T> let opQueue: NSOperationQueue let f:Future<UIImage> = opQueue.add { return functionThatBuildsUImage() }</code></pre> <p>NSOperation 写起来很烦人,但是当您使用诸如 Future 之类的实现方式的时候,那么写起来就会很容易了。</p> <p>让我们说一说我的想法,我不想使用 FIFO,也不是对并行层级进行控制。假设,我有一个图像操作的批处理集,并且我知道在我的 iPad 上有三个核心,然后我想要同时运行两个图像操作。我或许会使用 NSOperationQueue ,将兵法层级设定为 2,然后我就可以添加运行这些代码块的方法,从而在 NSOperationQueue 内部运行。</p> <p>现在我们使用 Future 来干掉这些恼人的委托。如果大家看过这些委托代码的话,就会意识到委托是异步操作当中最可怕的东西之一,它会将您的 iOS 或者 macOS 代码变得杂乱不清的毛线团,因为代码逻辑将会缠绕在一起,很难分离。</p> <p>有些时候人们甚至还会与视图控制器进行交互,当视图控制器完成之后,就会有某种回调的委托来通知您结果如何。我们要做的就是给视图控制器添加一个属性,如果您想要知道用户何时选择了某样东西的话,那么您需要创建一个 Future。如果要进行操作的东西有很多不同的输出的话,那么我们可以创建一个枚举值。用户选取之后,就会生成 Future 结果。</p> <pre> <code class="language-javascript">public extension SignalProducerType { public func mapFuture<U>(transform: Value -> Future<U>?) -> SignalProducer<U?, Error> }</code></pre> <p>关于 Future 和 <strong>Reactive</strong> 之间的区别,有很多人问了我这个问题了。但事实上,Future 和 Reactive 可以很好的协同工作,只要您了解何时使用 Future,何时使用 Reactive 即可。</p> <p>我发现很多人使用 Reactive 代码来完成本该是 Future 所做的事,因为他们知道这些工作该如何在 Reactive 当中进行。假设我有一个会随时变化的信号量,并且我想要进行监听,那么您就需要使用 Reactive 了,例如 RxSwift 或者 ReactiveCocoa。</p> <p>如果您只是到后台当中执行操作,然后返回一个简单的结果,最后就把信号量给关闭掉,那么您应该使用 Future 而不是 Reactive,因为 FutureKit 当中的可组合性正是您所希望的。</p> <p>如果您尝试将这些异步第三方库当中的异步操作给组合起来,您会发现这会非常、非常困难。而使用 Future 的话,你只需要匹配和回取数据,然后在后台执行,最后就可以得到一个结果,万事大吉。</p> <h3><strong>问答时间到</strong></h3> <p><strong>问:可否动态组合 Future 呢?也就是说,当您需要向服务器进行某些 Pull 操作,但是在操作完成之前又无法知道需要多少 Pull 操作的时候,对于 API 的使用者来说,能否用简单的 Future 来完成这项操作呢?</strong></p> <p>Michael:这个时候您需要使用 Promise。您不应该去试图组合 onComplete 和 onSuccess ,我们已经有一个方法表明可以前往服务器,然后尝试拉取数据,接着如果读取失败的话就返回,而不应该去封装这个步骤,我们只要找到这个所需的 Promise 对象即可,非常简单,呃,我需要尝试多少次呢?完全无需进行重试,睡一觉起来再试就行了。所以实际上这个问题并不是很困难,但是正如您所说的,这并不是一个很直观的思路。</p> <p><strong>问:我所开发的 App 已经用了很多 Reactive 了,比如 RxSwift。我在想您能否分享一些 Future/Promise 与 Reactive/Rx 之间互用的编程范式呢?</strong></p> <p>Michael:典型的例子就是:您可能会需要进行一系列操作,然后您想要将其插入到某个异步操作当中。那么使用 Future 和 ReactiveCocoa 来构建动作是非常简单的。与此同时,也有很多 Reactive 能实现而 Future 不能实现的操作。我通常情况下会告诉人们,Future 的好处在于降低组合的难度。我们可以把多个 Future 组合在一起,然后同时等到一个期望的结果。这就是 Future 的优势所在。</p> <h3><strong>参考资料</strong></h3> <ul> <li><a href="/misc/goto?guid=4959719947989183939" rel="nofollow,noindex">FutureKit for Swift</a></li> </ul> <p> </p> <p> </p> <p>来自:http://www.tuicool.com/articles/ZfAf6vN</p> <p> </p>