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 的 the 惯用 Swift 实现。 我们的大胆目标 (BHAG) 是被复制粘贴到 Swift 标准库中。

BrightFutures 的稳定性已通过在生产中的广泛使用得到证明。 它目前在多个应用程序中使用,每月活跃用户总数接近 50 万。 如果您在生产中使用 BrightFutures,我们很乐意听到您的消息!

最新消息

Join the chat at https://gitter.im/Thomvis/BrightFutures GitHub Workflow tests.yml status badge Carthage compatible CocoaPods version CocoaPods

BrightFutures 8.0 现已可用! 此更新增加了 Swift 5 兼容性。

安装

CocoaPods

  1. 将以下内容添加到您的 Podfile

    pod 'BrightFutures'
  2. 使用框架集成您的依赖项:将 use_frameworks! 添加到您的 Podfile 中。

  3. 运行 pod install

Carthage

  1. 将以下内容添加到您的 Cartfile

    github "Thomvis/BrightFutures"
    
  2. 运行 carthage update 并按照 Carthage 自述文件 中描述的步骤操作。

文档

示例

我们编写了很多异步代码。 无论是等待从网络接收数据,还是希望在主线程之外执行昂贵的计算,然后更新 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.logInPosts.fetchPosts 都会立即返回一个 Future。 Future 可能因错误而失败,也可能因值而成功,该值可以是任何类型,从 Int 到您的自定义结构体、类或元组。 您可以保留 future 并注册回调,以便在 future 成功或失败时方便地收到通知。

当从 User.logIn 返回的 future 失败时,例如用户名和密码不匹配,flatMaponSuccess 将被跳过,并且 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 作为方法的返回类型,这样所有调用站点都可以受益。 这将在下一节中解释。

提供 Futures

现在让我们扮演一个想要使用 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 的结果:onCompleteonSuccessonFailure。 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

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

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,该 Future 将提供用于恢复的值。

实用功能

BrightFutures 还提供了一些实用功能,可以简化使用多个 future 的过程。 这些功能被实现为自由(即全局)函数,以解决 Swift 当前的限制。

Fold

内置的 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

使用 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

traversemapfold 组合在一个方便的函数中。 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

delay 返回一个新的 Future,它将在等待给定的时间间隔后完成,并返回先前 Future 的结果。 为了简化使用 DispatchTimeDispatchTimeInterval,我们建议使用此 扩展

Future<Int, Never>(value: 3).delay(2.seconds).andThen { result in
    // execute after two additional seconds
}

默认线程模型

BrightFutures 尽力提供一个简单而合理的默认线程模型。 理论上,所有线程都是平等创建的,BrightFutures 不应该关心它位于哪个线程上。 但实际上,主线程比其他线程更平等,因为它在我们心中占有特殊的位置,并且您经常希望它执行 UI 更新。

Future 上的许多方法都接受一个可选的执行上下文和一个块,例如 onSuccessmaprecover 等等。 该块(在 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 的 BFTasksScala 中的 Promises & Futures 实现以及 Max Howell 的 PromiseKit

许可

BrightFutures 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。