精简的 Future<Value, Error>
实现
突出的Future
代表任务的结果,可能已经可用,可能在未来,也可能永不。 FutureX 通过考虑到人机工程和性能,提供了一个精简的 Future<Value, Error>
类型。
未来使用类似 map
、flatMap
、zip
、reduce
等熟悉的函数来组合任务,这些都是易于学习和使用的。
入门
- 快速入门指南 — 概述 · 创建未来 · 附加回调 ·
等待
- 函数组合 —
map
·flatMap
·mapError
·flatMapError
·zip
·reduce
- 扩展 —
first
·forEach
·after
·retry
·materialize
- 线程 · 取消 · Async/Await · 性能
快速入门指南
让我们先了解一下可用的类型。当然,中心是 Future
和它的 Result
。
struct Future<Value, Error> {
var result: Result? { get }
func on(success: ((Value) -> Void)?, failure: ((Error) -> Void)?, completion: (() -> Void)?)
enum Result {
case success(Value), failure(Error)
}
}
Future
由两个泛型参数 -Value
和Error
参数化。这使我们能够利用 Swift 的类型安全功能,并使用Never
模型永不失败的未来 -Future<Value, Never>
。
创建未来
通常,你会使用一个 Promise
来创建未来。
func someAsyncTask() -> Future<Value, Error> {
let promise = Promise<Value, Error>()
performAsyncTask { value, error in
// If success
promise.succeed(value: value)
// If error
promise.fail(error: error)
}
return promise.future
}
Promise
是线程安全的。你可以从任何线程调用succeed
或fail
,并且可以调用任意次数——只有第一次的结果会被发送给Future
。
如果工作结果在创建未来时已经可用,你可以使用以下一些初始化器:
// Init with a value
Future(value: 1) // Inferred to be Future<Int, Never>
Future<Int, MyError>(value: 1)
// Init with an error
Future<Int, MyError>(error: .dataCorrupted)
// Init with a throwing closure
Future<Int, Error> {
guard let value = Int(string) else {
throw Error.dataCorrupted
}
return value
}
这些
init
方法不需要任何内存分配,因此它们的速度非常快,甚至比分配Promise
实例更快。
附加回调
要为 Future
附加回调(每个都是可选的),请使用 on
方法。
let future: Future<Value, Error>
future.on(success: { print("received value: \($0)" },
failure: { print("failed with error: \($0)" }),
completion: { print("completed" })
如果未来已经有一个结果,则回调会立即执行。如果未来还没有结果,那么回调将在未来被解析时执行。未来保证只能解析一次结果,回调也保证只运行一次。
默认情况下,回调在 .main
调度程序上运行。如果任务在主线程上完成,回调会立即执行。否则,它们会被分派到在主线程上异步执行。
有关理由和更多信息,请参阅 线程。
wait
使用 wait
方法可以使当前线程阻塞,等待未来接收结果。
let result = future.wait() // Mostly useful for testing and debugging
result
如果未来已经有了结果,你可以同步读取它。
struct Future<Value, Error> {
var value: Value? { get }
var error: Error? { get }
var result: Result? { get }
}
函数式组合
map
、flatMap
使用熟悉的 map
和 flatMap
函数来转换未来值得并链式调用未来
let user: Future<User, Error>
func loadAvatar(url: URL) -> Future<UIImage, Error>
let avatar = user
.map { $0.avatarURL }
.flatMap(loadAvatar)
如果您不熟悉 flatMap
,一开始可能难以理解。但一旦理解了,使用它就变得自然而然。
实际上不仅有单个的
flatMap
,还有几个flatMap
变体。额外的变体允许您无缝地混合可能产生错误和不会产生错误的未来。
mapError
、flatMapError
Future
有类型化错误。要从一个错误类型转换到另一个错误类型,请使用 mapError
let request: Future<Data, URLError>
request.mapError(MyError.init(urlError:))
使用 flatMapError
来“恢复”错误。
如果您有一个永远不会产生错误的未来 (
Future<_, Never>
),您可以将其强制转换为可以产生 任何 错误的未来,使用castError
方法。但在大多数情况下,这并不是必需的。
zip
使用 zip
来结合最多三个未来的结果,在单个未来中
let user: Future<User, Error>
let avatar: Future<UIImage, Error>
Future.zip(user, avatar).on(success: { user, avatar in
// use both values
})
也可以等待多个未来的结果
Future.zip([future1, future2]).on(success: { values in
// use an array of values
})
reduce
使用 reduce
来结合多个未来的结果
let future1 = Future(value: 1)
let future2 = Future(value: 2)
Future.reduce(0, [future1, future2], +).on(success: { value in
print(value) // prints "3"
})
扩展
除了主要接口外,还有一系列扩展的函数集,用于 Future
。并不是所有这些都在这里提及,请查看 FutureExtensions.swift
以获取更多信息!
first
使用 first
来等待第一个未来任务成功
let requests: [Future<Value, Error>]
Future.first(requests).on(success: { print("got response!") })
forEach
使用 forEach
来按顺序执行任务
// `startWork` is a function that returns a future
Future.forEach([startWork, startOtherWork]) { future in
// In the callback you can subscribe to each future when work is started
future.on(success: { print("work is completed") })
}
after
使用 after
在给定的时间间隔后生成值。
Future.after(seconds: 2.5).on { print("2.5 seconds passed") })
retry
使用 retry
来执行给定次数的尝试以成功完成工作。
func startSomeWork() -> Future<Value, Error>
Future.retry(attempts: 3, delay: .seconds(3), startSomeWork)
重试非常灵活。它允许你指定多种延迟策略,包括指数退避,在重试之前检查错误等等。
materialize
这个功能非常有趣。它将 Future<Value, Error>
转换为 Future<Future<Value, Error>.Result, Never>
–一个永远不会失败的未来。它总是以初始未来的结果成功完成。现在,你为什么想要这样做呢?事实上,materialize
与其他函数如 zip
、reduce
、first
等组合得非常好。所有这些函数会在给定的任何一个未来失败时立即失败。但是与 materialize
一起,你可以更改这些函数的行为,使它们会等待直到所有未来都解决,无论是成功还是错误。
请注意,我们使用本地的
Never
类型来表示永远不会产生错误的情况。
Future.zip(futures.map { $0.materialize() }).on { results in
// All futures are resolved and we get the list of all of the results -
// either values or errors.
}
多线程
在iOS上,用户期望UI渲染是同步发生的。为了满足这一点,默认情况下,回调使用Scheduler.main
来运行。如果在工作线程上运行工作,则立即运行,否则异步地在主线程上运行。其设计类似于RxSwift等响应式框架。它为使用设计时即为异步的future开辟了全新的领域。
有三个调度器可用
enum Scheduler {
/// If the task finishes on the main thread, the callbacks are executed
/// immediately. Otherwise, they are dispatched to be executed
/// asynchronously on the main thread.
static var main: ScheduleWork
/// Immediately executes the given closure.
static var immediate: ScheduleWork
/// Runs asynchronously on the given queue.
static func async(on queue: DispatchQueue, flags: DispatchWorkItemFlags = []) -> ScheduleWork
}
ScheduleWork
只是个函数,因此您可以轻松地提供自定义实现。
要更改回调调用的调度器,请使用observe(on:)
// There are two variants, one with `DispatchQueue`, one with `Scheduler`.
// Here's the one with `DispatchQueue`:
future.observe(on: .global())
on(success: { print("value: \($0)" })
您也可以使用observe(on:)
在后台队列上执行转换,例如map
、tryMap
等。
future.observe(on: .global())
.map { /* heavy operation */ }
请注意,只有通过
observe(on:)
直接返回的future才能确保在给定的队列(或调度器)上运行其延续者。
取消
取消是一个与Future
正交的关心点。将Future
视为简单的回调替代品——回调不支持取消。
FutureX实现了CancellationToken
模式,以实现协作取消异步任务。通过取消令牌源创建令牌。
let cts = CancellationTokenSource()
asyncWork(token: cts.token).on(success: {
// To prevent closure from running when task is cancelled use `isCancelling`:
guard !cts.isCancelling else { return }
// Do something with the result
})
// At some point later, can be on the other thread:
cts.cancel()
要取消多个异步任务,您可以传递相同的令牌给所有这些任务。
实现支持取消的异步任务很容易。
func loadData(with url: URL, _ token: CancellationToken = .none) -> Future<Data, URLError> {
let promise = Promise<Data, URLError>()
let task = URLSession.shared.dataTask(with: url) { data, error in
// Handle response
}
token.register(task.cancel)
return promise.future
}
任务对取消有完全的控制。您可以忽略它,可以使用特定的错误失败承诺,返回部分结果,或者根本不解决承诺。
CancellationTokenSource
本身是用Future
构建的,并受益于其所有性能优化。
Async/Await
Async/await通常建立在future之上。当Swift最终添加async/await支持时,它会相对容易地将使用futures的代码替换为async/await。
有一个基于FutureX的(阻塞)async/await的版本。它不需要在生产环境中使用。
性能
FutureX中的每个功能都是考虑到性能而设计的。
我们避免动态分发,减少分配和回收的数量,避免做不必要的操作并尽可能减少锁定。方法通常以不那么优雅但更高效的方式实现。
此外,还有一些关键的设计差异使得 FutureX 在其他框架中具有优势。一个例子就是 Future
类型本身,它被设计为结构体,允许在不进行单个分配的情况下执行一些常见操作。
需求
FutureX | Swift | Xcode | 平台 |
---|---|---|---|
FutureX 1.1 | Swift 5.0 | Xcode 10.2 | iOS 10.0 / watchOS 3.0 / macOS 10.12 / tvOS 10.0 |
FutureX 1.0 | Swift 4.2 – 5.0 | Xcode 10.1 – 10.2 | iOS 10.0 / watchOS 3.0 / macOS 10.12 / tvOS 10.0 |
FutureX 0.17 | Swift 4.0 – 4.2 | Xcode 9.2 – 10.1 | iOS 9.0 / watchOS 2.0 / macOS 10.11 / tvOS 9.0 |
许可
FutureX 在 MIT 许可下提供。有关更多信息,请参阅 LICENSE 文件。