BrightFutures
BrightFutures 已经达到生命周期的尾声。经过一段长时间的开发活动有限,Swift 的 Async/Await 使得库变得不再必要。请考虑从 BrightFutures 迁移到 async/await。当这样做时,async get()
方法将证明了它的有用性
// in an async context...
let userFuture = User.logIn(username, password)
let user = try await userFuture.get()
// or simply:
let posts = try await Posts.fetchPosts(user).get()
README 的其余部分最近没有更新,但出于历史原因被保留。
您如何利用 Swift 的力量编写出优秀的异步代码?BrightFutures 是我们的答案。
BrightFutures 在 Swift 中实现了经过验证的 函数式概念,提供了一种强大的替代方案来完成块并提供类型安全错误处理。
BrightFutures 的目标是成为 futures 和 promises 的 典范 Swift 实现。我们的宏伟目标(BHAG)是复制粘贴到 Swift 标准库。
BrightFutures 的稳定性已在生产中得到验证,已在几个应用中使用,总共有近 50 万月活跃用户。如果您在生产中使用 BrightFutures,我们很想了解您的情况!
最新消息
BrightFutures 8.0 现在可用!此更新增加了对 Swift 5 的兼容性。
安装
CocoaPods
-
将以下内容添加到您的 Podfile
pod 'BrightFutures'
-
使用框架集成依赖项:将
use_frameworks!
添加到您的 Podfile。 -
运行
pod install
。
Carthage
-
将以下内容添加到您的 Cartfile
github "Thomvis/BrightFutures"
-
运行
carthage update
并按照在 Carthage 的 README 中描述的步骤操作。
文档
- 本 README 涵盖了 BrightFutures 几乎所有的功能
- 测试 包含了每个功能的(简单)使用示例(测试覆盖率 97%)
- 主要作者 Thomas Visser 在 2015 年 4 月的 CocoaHeadsNL Meetup 上做了一场演讲
- Highstreet Watch App(一个开源的 WatchKit App)重度使用了 BrightFutures 的早期版本,它存放在 这里
示例
我们编写了许多异步代码。无论是等待从网络传来的数据,还是希望在主线程上执行昂贵的计算并在完成后更新 UI,我们往往都进行“一次性触发并回调”的操作。以下是典型的异步代码片段:
User.logIn(username, password) { user, error in
if !error {
Posts.fetchPosts(user, success: { posts in
// do something with the user's posts
}, failure: handleError)
} else {
handleError(error) // handeError is a custom function to handle errors
}
}
现在让我们看看 BrightFutures 可以为您做些什么
User.logIn(username, password).flatMap { user in
Posts.fetchPosts(user)
}.onSuccess { posts in
// do something with the user's posts
}.onFailure { error in
// either logging in or fetching posts failed
}
User.logIn
和 Posts.fetchPosts
现在都立即返回一个 Future
。未来可以失败并带有错误,或者成功并带有值,这个值可以是任何从 Int 到自定义结构体、类或元组的值。您可以保留未来,并在方便的时候注册对成功或失败时的回调。
当从 User.logIn
返回的未来失败时,例如用户名和密码不匹配,则跳过 flatMap
和 onSuccess
,并且 onFailure
会调用在登录过程中发生的错误。如果登录尝试成功,则结果用户对象传递给 flatMap
,将其“转变”为他的或她的帖子数组。如果帖子无法获取,则跳过 onSuccess
,并且 onFailure
会调用在获取帖子时发生的错误。如果帖子能够成功获取,则 onSuccess
会调用用户的帖子。
这只是冰山一角。更多的示例和技术可以在本说明书中找到,通过浏览测试或查看官方补充框架 FutureProofing。
包装表达式
如果您已经有了只需要异步执行并且需要 Future 来表示其结果的函数(或任何表达式),您可以轻松地将其包装在 asyncValue
块中
DispatchQueue.global().asyncValue {
fibonacci(50)
}.onSuccess { num in
// value is 12586269025
}
asyncValue
是在 GCD 的 DispatchQueue
拓展中定义的。虽然这非常简短且简单,但同样有限。在许多情况下,您需要一种方式来指示任务失败。为此,您可以通过返回一个 Result 来代替返回值。结果可以指示成功或失败
enum ReadmeError: Error {
case RequestFailed, TimeServiceError
}
let f = DispatchQueue.global().asyncResult { () -> Result<Date, ReadmeError> in
if let now = serverTime() {
return .success(now)
}
return .failure(ReadmeError.TimeServiceError)
}
f.onSuccess { value in
// value will the NSDate from the server
}
未来块需要一个显式的类型,因为 Swift 编译器无法推断多语句块的类型。
与其包装现有表达式,通常更好的方法是使用 Future 作为方法的返回类型,这样所有调用点都可以从中受益。这将在下一节中解释。
提供未来
现在让我们假设您是一位想使用 BrightFutures 的 API 作者。Future 是设计为只读的,除创建 Future 的位置外。这是通过 Future 上接受一个闭包的初始化器来实现的,该闭包在完成作用域中,您可以在其中完成 Future。完成作用域有一个参数也是闭包,用于在 Future 中设置值(或错误)。
func asyncCalculation() -> Future<String, Never> {
return Future { complete in
DispatchQueue.global().async {
// do a complicated task and then hand the result to the promise:
complete(.success("forty-two"))
}
}
}
Never
表示 Future
不能失败。这是由类型系统保证的,因为 Never
没有初始化器。作为完成作用域的替代,您还可以创建一个 Promise
,它是 Future 的可写等效,并将其存储以供以后使用。
回调
您可以通过注册回调来了解Future
的结果:onComplete
、onSuccess
和onFailure
。回调在future完成后的执行顺序没有保证,但保证回调会串行执行。在同一个future的回调中添加新的回调是不安全的。
链式回调
在Future
上使用andThen
函数可以显式定义回调的顺序。传递给andThen
的闭包旨在执行副作用,并不影响结果。andThen
返回一个在闭包执行后完成的新Future,它的结果与当前future相同。
var answer = 10
let _ = Future<Int, Never>(value: 4).andThen { result in
switch result {
case .success(let val):
answer *= val
case .failure(_):
break
}
}.andThen { result in
if case .success(_) = result {
answer += 2
}
}
// answer will be 42 (not 48)
函数组合
map
map
函数返回一个新Future,如果当前Future失败,则包含错误,或者返回从给定闭包应用到的当前Future的值的返回值。
fibonacciFuture(10).map { number -> String in
if number > 5 {
return "large"
}
return "small"
}.map { sizeString in
sizeString == "large"
}.onSuccess { numberIsLarge in
// numberIsLarge is true
}
flatMap
flatMap
用于将future的结果映射到新future的值。
fibonacciFuture(10).flatMap { number in
fibonacciFuture(number)
}.onSuccess { largeNumber in
// largeNumber is 139583862445
}
zip
let f = Future<Int, Never>(value: 1)
let f1 = Future<Int, Never>(value: 2)
f.zip(f1).onSuccess { a, b in
// a is 1, b is 2
}
filter
Future<Int, Never>(value: 3)
.filter { $0 > 5 }
.onComplete { result in
// failed with error NoSuchElementError
}
Future<String, Never>(value: "Swift")
.filter { $0.hasPrefix("Sw") }
.onComplete { result in
// succeeded with value "Swift"
}
从错误中恢复
如果 Future
失败,可以使用 recover
提供默认值或备选值并继续回调链。
// imagine a request failed
Future<Int, ReadmeError>(error: .RequestFailed)
.recover { _ in // provide an offline default
return 5
}.onSuccess { value in
// value is 5 if the request failed or 10 if the request succeeded
}
除了 recover
,还可以使用 recoverWith
提供一个将提供恢复值时的未来的 Future。
工具函数
BrightFutures 还提供了一些实用函数来简化处理多个未来任务的工作。这些函数作为免费(即全局)函数实现,以应对 Swift 当前的限制。
Fold
内建的 fold
函数允许您通过在列表的每个元素上执行操作将其转换为单个值,并将该元素作为它添加到结果值时消耗它。一个简单的 fold 用例可能是计算整数列表的总和。
使用内建 fold
函数折叠 Future 列表不太方便,因此 BrightFutures 提供了一个专门针对我们的用例设计的 fold
函数。BrightFutures 的 fold
将 Future 列表转换成一个包含结果值的单个 Future。这使得我们可以,例如,计算斐波那契数列前 10 个元素的求和。
let fibonacciSequence = [fibonacciFuture(1), fibonacciFuture(2), ..., fibonacciFuture(10)]
// 1+1+2+3+5+8+13+21+34+55
fibonacciSequence.fold(0, f: { $0 + $1 }).onSuccess { sum in
// sum is 143
}
序列
使用 sequence
,您可以将一系列 future 转换为包含从那些 future 的结果列表的单个 future。
let fibonacciSequence = [fibonacciFuture(1), fibonacciFuture(2), ..., fibonacciFuture(10)]
fibonacciSequence.sequence().onSuccess { fibNumbers in
// fibNumbers is an array of Ints: [1, 1, 2, 3, etc.]
}
遍历
traverse
在一个便捷的函数中将 map
和 fold
结合起来。 traverse
接收一个值列表和一个闭包,该闭包从该列表中取出单个值并将其转换为 future。 traverse
的结果是一个包含由给定的闭包返回的 future 的值数组的单个 future。
(1...10).traverse {
i in fibonacciFuture(i)
}.onSuccess { fibNumbers in
// fibNumbers is an array of Ints: [1, 1, 2, 3, etc.]
}
延迟
delay
返回一个新的 future,在等待给定间隔后,将完成上一个 future 的结果。要简化对 DispatchTime
和 DispatchTimeInterval
的使用,我们建议使用此 扩展。
Future<Int, Never>(value: 3).delay(2.seconds).andThen { result in
// execute after two additional seconds
}
默认线程模型
BrightFutures 尽力提供一个简单而合理的默认线程模型。理论上,所有线程都是平等的,BrightFutures 不应该关心它在哪个线程上。然而,在实际操作中,主线程是其他线程的 ‘优势地位’,因为它在我们心中有一个特殊的位置,并且你通常会希望在它上进行 UI 更新。
许多 Future
方法都接受一个可选的 执行上下文 和一个块,例如 onSuccess
、map
、recover
等。该块在给定的执行上下文中执行(当 future 完成),在实践中是一个 GCD 队列。如果没有明确提供上下文,将遵循以下规则来确定使用的执行上下文
- 如果方法是在主线程中调用的,则将在主队列上执行块
- 如果方法不是从主线程调用的,则块将在全局队列上执行
如果您想获取自定义的线程行为,请跳过此部分。更多信息请访问
自定义执行上下文
可以通过提供显式的执行上下文来覆盖默认的线程行为。您可以选择任何内置的上下文,或者轻松创建自己的上下文。默认上下文包括:任何发送队列,任何NSOperationQueue
以及当您不想切换线程/队列时的ImmediateExecutionContext
。
let f = Future<Int, Never> { complete in
DispatchQueue.global().async {
complete(.success(fibonacci(10)))
}
}
f.onComplete(DispatchQueue.main.context) { value in
// update the UI, we're on the main thread
}
尽管将来的操作来自全局队列,但完成闭包将在主队列上被调用。
无效化令牌
无效化令牌可以用来撤销回调,防止在未来的完成时被调用。这在回调被释放的上下文经常变化且快速的情况下特别有用,例如在可重复使用的视图中,如表格视图和集合视图单元格。以下是一个例子
class MyCell : UICollectionViewCell {
var token = InvalidationToken()
public override func prepareForReuse() {
super.prepareForReuse()
token.invalidate()
token = InvalidationToken()
}
public func setModel(model: Model) {
ImageLoader.loadImage(model.image).onSuccess(token.validContext) { [weak self] UIImage in
self?.imageView.image = UIImage
}
}
}
通过每次复用时无效化令牌,我们可以防止在设置下一个模型后设置前一个模型的图片。
无效化令牌不会取消未来表示的任务。这是一个不同的问题。使用无效化令牌,结果仅仅是被忽略。在原始未来完成后无效化令牌不会做任何事情。
如果您正在寻找取消运行中的任务的方法,您可以考虑使用NSProgress。
鸣谢
BrightFutures的主要作者是Thomas Visser。他是Highstreet的首席iOS工程师。我们欢迎您的任何反馈和拉取请求。请在此列表上发表您的名字!
BrightFutures受到了Facebook的BFTasks、Scala中的Promises与Futures实现和在Scala以及Max Howell的PromiseKit的启发。
许可
BrightFutures 在MIT许可下可用。查看LICENSE文件以获取更多信息。