BrightFutures 8.2.0

BrightFutures 8.2.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布日期最新发布2022年7月
SPM支持 SPM

Thomas Visser 维护。



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,我们很想了解您的情况!

最新消息

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 的 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.logInPosts.fetchPosts 现在都立即返回一个 Future。未来可以失败并带有错误,或者成功并带有值,这个值可以是任何从 Int 到自定义结构体、类或元组的值。您可以保留未来,并在方便的时候注册对成功或失败时的回调。

当从 User.logIn 返回的未来失败时,例如用户名和密码不匹配,则跳过 flatMaponSuccess,并且 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的结果:onCompleteonSuccessonFailure。回调在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 在一个便捷的函数中将 mapfold 结合起来。 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 的结果。要简化对 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
}

尽管将来的操作来自全局队列,但完成闭包将在主队列上被调用。

无效化令牌

无效化令牌可以用来撤销回调,防止在未来的完成时被调用。这在回调被释放的上下文经常变化且快速的情况下特别有用,例如在可重复使用的视图中,如表格视图和集合视图单元格。以下是一个例子

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文件以获取更多信息。