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 的 the 惯用 Swift 实现。 我们的大胆目标 (BHAG) 是被复制粘贴到 Swift 标准库中。
BrightFutures 的稳定性已通过在生产中的广泛使用得到证明。 它目前在多个应用程序中使用,每月活跃用户总数接近 50 万。 如果您在生产中使用 BrightFutures,我们很乐意听到您的消息!
BrightFutures 8.0 现已可用! 此更新增加了 Swift 5 兼容性。
将以下内容添加到您的 Podfile 中
pod 'BrightFutures'
使用框架集成您的依赖项:将 use_frameworks!
添加到您的 Podfile 中。
运行 pod install
。
我们编写了很多异步代码。 无论是等待从网络接收数据,还是希望在主线程之外执行昂贵的计算,然后更新 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
。 Future 可能因错误而失败,也可能因值而成功,该值可以是任何类型,从 Int 到您的自定义结构体、类或元组。 您可以保留 future 并注册回调,以便在 future 成功或失败时方便地收到通知。
当从 User.logIn
返回的 future 失败时,例如用户名和密码不匹配,flatMap
和 onSuccess
将被跳过,并且 onFailure
将被调用,并显示登录时发生的错误。 如果登录尝试成功,则生成的 user 对象将传递给 flatMap
,它会将 user“转换”为他的帖子数组。 如果无法获取帖子,则会跳过 onSuccess
,并调用 onFailure
,并显示获取帖子时发生的错误。 如果成功获取了帖子,则会使用用户的帖子调用 onSuccess
。
这只是冰山一角。 可以在此自述文件中找到更多示例和技术,通过浏览测试或查看官方配套框架 FutureProofing。
如果您已经有一个函数(或实际上是任何表达式),您只想异步执行并有一个 Future 来表示其结果,您可以轻松地将其包装在 asyncValue
块中
DispatchQueue.global().asyncValue {
fibonacci(50)
}.onSuccess { num in
// value is 12586269025
}
asyncValue
在 GCD 的 DispatchQueue
的扩展中定义。 虽然这非常简短明了,但也同样有限。 在许多情况下,您需要一种指示任务失败的方法。 为此,您可以返回一个 Result,而不是返回值。 Results 可以指示成功或失败
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
}
future 块需要显式类型,因为 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 具有与此 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
返回一个新的 Future,如果此 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
用于将 future 的结果映射到新 Future 的值。
fibonacciFuture(10).flatMap { number in
fibonacciFuture(number)
}.onSuccess { largeNumber in
// largeNumber is 139583862445
}
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
}
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,该 Future 将提供用于恢复的值。
BrightFutures 还提供了一些实用功能,可以简化使用多个 future 的过程。 这些功能被实现为自由(即全局)函数,以解决 Swift 当前的限制。
内置的 fold
函数允许您通过对列表中的每个元素执行操作,将值列表转换为单个值,该操作在添加到结果值时消耗它。 fold 的一个简单的用例是计算整数列表的总和。
使用内置的 fold
函数折叠 Future 列表不是很方便,这就是 BrightFutures 提供一个特别适合我们用例的函数的原因。 BrightFutures 的 fold
将 Future 列表转换为一个包含结果值的 Future。 这允许我们例如计算斐波那契数列的前 10 个 Future 包装元素的总和
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,其中包含给定闭包返回的 Futures 中的值的数组。
(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
}
即使 future 是从全局队列完成的,完成闭包也会在主队列上调用。
失效令牌可用于使回调失效,防止其在 Future 完成时被执行。这在回调执行的上下文经常快速变化的情况下特别有用,例如可重用的视图,如表格视图和集合视图单元格。后者的一个例子是:
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
}
}
}
通过在每次重用时使令牌失效,我们可以防止在设置下一个模型之后设置上一个模型的图像。
失效令牌不会取消 Future 所代表的任务。那是另一个问题。使用失效令牌,结果仅仅是被忽略。在原始 Future 完成后使令牌失效不会产生任何影响。
如果您正在寻找取消正在运行的任务的方法,您可以考虑使用NSProgress。
BrightFutures 的主要作者是 Thomas Visser。他是 Highstreet 的首席 iOS 工程师。我们欢迎任何反馈和 pull request。让您的名字出现在此列表上!
BrightFutures 的灵感来自 Facebook 的 BFTasks、Scala 中的 Promises & Futures 实现以及 Max Howell 的 PromiseKit。
BrightFutures 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。