PINFuture
安装
PINFuture可以通过CocoaPods获得。要安装它,简单地将以下行添加到Podfile中
pod "PINFuture"
概述
PINFuture是“Future”这种异步原语的Objective-C实现。这个库与其他Objective-C的Future实现的不同之处在于它旨在用Objective-C泛型来保持类型安全。
什么是Future?
Future是一个“最终将准备就绪的值”的包装器。
Future对象可以有3种状态之一,通常从“待定”状态开始。 “待定”意味着Future的最终值尚不清楚,目前正在计算中。Future最终将过渡到“已实现”状态并包含最终值,或者过渡到“已拒绝”状态并包含错误对象。“已实现”和“已拒绝”是终止状态,Future的值/错误在首次实现或拒绝转换后不会改变。
示例
方法签名
回调样式
- (void)logInWithUsername:(NSString *)username
password:(NSString *)password
success:( void (^)(User *user) )successBlock
failure:( void (^)(NSError *error) )failureBlock;
Future样式
- (PINFuture<User *> *)logInWithUsername:(NSString *)username
password:(NSString *)password;
链式异步操作
回调样式
[self showSpinner];
[User logInWithUsername:username password:password success:^(User *user) {
[Posts fetchPostsForUser:user success:^(Posts *posts) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideSpinner];
// update the UI to show posts
});
} failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideSpinner];
// update the UI to show the error
});
}];
} failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideSpinner];
// update the UI to show the error
});
}];
Future样式
[self showSpinner];
PINFuture<User *> *userFuture = [User logInWithUsername:username password:password];
PINFuture<Posts *> *postsFuture = [PINFutureMap<User *, Posts *> flatMap:userFuture executor:[PINExecutor main] transform:^PINFuture<Posts *> *(User *user) {
return [Posts fetchPostsForUser:user];
}];
[postsFuture executor:[PINExecutor main] completion:^{
[self hideSpinner];
}];
[postsFuture executor:[PINExecutor main] success:^(Posts *posts) {
// update the UI to show posts
} failure:^(NSError *error) {
// update the UI to show the error
}];
在测试中模拟异步函数
回调样式
OCMStub([fileMock readContentsPath:@"foo.txt"
success:OCMOCK_ANY
failure:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
(void)(^successBlock)(NSString *) = nil;
[invocation getArgument:&successBlock atIndex:3];
if (successBlock) {
successBlock(@"fake contents");
}
});
Future样式
OCMStub([fileMock readContentsPath:@"foo.txt"]).andReturn([PINFuture<NSString *> withValue:@"fake contents"]);
处理值
要访问Future的最终值,请注册成功
和失败
回调。如果只想知道Future何时完成(而不特定于值或错误),请注册一个完成
回调。
回调将按照它们注册的顺序进行分发。然而,根据指定的执行器
,块可能以不同的顺序或并发执行。
线程模型
每次传递回调块时,都必须同时传递一个必需的 executor:
参数。 executor
决定了你的块在哪里以及何时执行。
executor:
值
常见的 [PINExecutor main]
在主 GCD 队列上执行一个块。[PINExecutor background]
从后台线程池中执行一个块。注意:使用此执行器时,单个 future 附着的两个回调块可能会并发执行。
一个有用的规则:如果你的回调块执行的工作是线程安全的 并且 不需要在主线程上执行(例如,因为它正在操作 UIKit),则使用 [PINExecutor background]
。
保留类型安全性
PINFuture 使用 Objective-C 泛型来维护与回调相同的类型安全性。
[PINFuture<NSNumber *> withValue:@"foo"]; // Compile error. Good!
在 Objective-C 中,类型参数是可选的。为 PINFuture 指定它们是一种好的实践。
[PINFuture withValue:@"foo"]; // This compiles but will likely blow up with "unrecognized selector" when the value is used.
在结果上阻塞
PINFuture 是非阻塞的,并提供没有阻塞机制。在异步值的计算上阻塞线程通常不是一个好的做法,但可以使用 Grand Central Dispatch Semaphores 实现。
处理异常
PINFuture 不捕获回调抛出的异常。在 PINFuture 针对的平台上, NSException
通常会终止程序。PINFuture 处理 NSError
。
API参考
构建
withValue:
构建一个已经完成的Future并赋予其一个值。
PINFuture<NSString *> stringFuture = [PINFuture<NSString *> withValue:@"foo"];
withError:
构建一个已经拒绝的Future并赋予其一个错误。
PINFuture<NSString *> stringFuture = [PINFuture<NSString *> withError:[NSError errorWithDescription:...]];
withBlock:
构建一个Future,并通过调用两个回调函数之一来满足或拒绝它。此方法通常不安全,因为没有强制要求您的块调用 resolve
或 reject
。这在编写基于Future的回调方法包装器时最为有用。您会发现此方法在Cocoa API的PINFuture包装器中使用得非常广泛。
PINFuture<NSString *> stringFuture = [PINFuture<NSString *> withBlock:^(void (^ fulfill)(NSString *), void (^ reject)(NSError *)) {
[foo somethingAsyncWithSuccess:resolve failure:reject];
}];
executor:block:
通过执行一个返回Future的块来构造一个Future。这种用法最常见的场景是将一些计算密集型工作从当前线程中派遣出去。 Whenever you can return a Future,你应该优先使用这种方法而不是withBlock:
,因为编译器可以强制执行你的block中的所有代码路径都将返回一个Future。
PINFuture<NSNumber *> fibonacciResultFuture = [PINFuture<NSNumber *> executor:[PINExecutor background] block:^PINFuture *() {
NSInteger *fibonacciResult = [self computeFibonacci:1000000];
return [PINFuture<NSNumber *> withValue:fibonacciResult];
}];
变换
为了在类型安全的操作如map
中转换值类型,我们需要跳过一些步骤,这是因为Objective-C对泛型的支持是基本性的。map
和flatMap
是PINFutureMap
类上的类方法。PINFutureMap
类有两个类型参数:FromType
和ToType
。
变换中的错误处理
map
和flatMap
只在进行转换时执行,前提是源Future是fulfilled的。如果源Future是rejected的,那么原始错误将被简单传递到返回值中。mapError
和flatMapError
只在进行转换时执行,前提是源Future是rejected的。如果源Future是fulfilled的,那么原始值将被简单传递到返回值中。
map
PINFuture<NSString *> stringFuture = [PINFutureMap<NSNumber *, NSString *> map:numberFuture executor:[PINExecutor background] transform:^NSString *(NSNumber * number) {
return [number stringValue];
}];
flatMap
PINFuture<UIImage *> imageFuture = [PINFutureMap<User *, UIImage *> flatMap:userFuture executor:[PINExecutor background] transform:^PINFuture<NSString *> *(User *user) {
return [NetworkImageManager fetchImageWithURL:user.profileURL];
}];
mapError
PINFuture<NSString *> *stringFuture = [File readUTF8ContentsPath:@"foo.txt" encoding:EncodingUTF8];
stringFuture = [fileAFuture executor:[PINExecutor immediate] mapError:^NSString * (NSError *errror) {
return ""; // If there's any problem reading the file, continue processing as if the file was empty.
}];
flatMapError
PINFuture<NSString *> *stringFuture = [File readUTF8ContentsPath:@"tryFirst.txt"];
stringFuture = [fileAFuture executor:[PINExecutor background] flatMapError:^PINFuture<NSString *> * (NSError *errror) {
if ([error isKindOf:[NSURLErrorFileDoesNotExist class]) {
return [File readUTF8ContentsPath:@"trySecond.txt"];
} else {
return [PINFuture withError:error]; // Pass through any other type of error
}
}];
聚集
gatherAll
NSArray<NSString *> fileNames = @[@"a.txt", @"b.txt", @"c.txt"];
NSArray<PINFuture<NSString *> *> *fileContentFutures = [fileNames map:^ PINFuture<NSString *> *(NSString *fileName) {
return [File readUTF8ContentsPath:fileName];
}];
PINFuture<NSArray<NSString *> *> *fileContentsFuture = [PINFuture<NSString *> gatherAll:fileContentFutures];
[fileContentsFuture executor:[PINExecutor main] success:^(NSArray<NSString *> *fileContents) {
// All succceeded.
} failure:^(NSError *error) {
// One or more failed. `error` is the first one to fail.
}];
gatherSome
实验性。此API可能会更改为提高类型安全。
NSArray<NSString *> fileNames = @[@"a.txt", @"b.txt", @"c.txt"];
NSArray<PINFuture<NSString *> *> *fileContentFutures = [fileNames map:^ PINFuture<NSString *> *(NSString *fileName) {
return [File readUTF8ContentsPath:fileName];
}];
PINFuture<NSArray *> *fileContentsOrNullFuture = [PINFuture<NSString *> gatherSome:fileContentFutures];
[fileContentsFuture executor:[PINExecutor main] success:^(NSArray *fileContents) {
// fileContents is an array of either `NSString *` or `[NSNull null]` depending on whether the source future resolved or rejected.
} failure:^(NSError *error) {
// This can't be reached. If any of the source futures fails, there will be a `[NSNull null]` entry in the array.
}];
链式副作用(必要之恶)
chainSuccess:failure:
这与success:failure
类似,但会返回一个新的Future,它在执行副作用之前不会完成或拒绝。应小心使用。通常不需要副作用,更少需要等待副作用。
// Fetch a user, and return a Future that resolves only after all NotificationCenter observers have been notified.
PINFuture<User *> *userFuture = [self userForUsername:username];
userFuture = [userFuture executor:[PINExecutor main] chainSuccess:^(User *user) {
[[NSNotifcationCenter sharedCenter] postNotification:kUserUpdated object:user];
} failure:nil;
return userFuture;
便捷方法(实验性)
在主线程执行
/在后台执行
我们观察到应用程序代码几乎总是调用executor:[PINExecutor main]
或executor:[PINExecutor background]
。对于每个接受executor:
的方法,都有两种变体:executeOnMain
和executeOnBackground
,它们稍微更简洁(比原来的短22个字符)。
以下是对应的调用对,每个对中的第二个调用演示了便利方法。
[userFuture executor:[PINExecutor main] success:success failure:failure];
[userFuture executeOnMainSuccess:success failure:failure];
[userFuture executor:[PINExecutor background] success:success failure:failure];
[userFuture executeOnBackgroundSuccess:success failure:failure];
PINFuture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executor:[PINExecutor main] transform:transform];
PINFuture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executeOnMainTransform:transform];
PINFuture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executor:[PINExecutor background] transform:transform];
PINFuture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executeOnBackgroundTransform:transform];
路线图
- 支持取消计算值的计算
- 任务原始数据
“未来”与“回调”
- 对于返回Future的函数,编译器可以强制在所有代码路径上返回一个值。在回调的情况下,无法强制执行所有代码路径应该通过调用一个精确回调结束的约定。
- Future保证了回调不会被调用多次。这在一个具有调用回调的副作用的函数中是一个很难强制执行的约定。
- 明确地说明回调的派发位置,可以避免与总是将回调派发到主队列的函数相比,在主队列上产生不必要的瓶颈。
“未来”与“任务”
- Future:值是主动计算的。即使没有消费者使用该值,Future值的计算工作仍然会发生。
- Task:值仅在调用
run
时才计算。
替代方案
Swift
- BrightFutures https://github.com/Thomvis/BrightFutures
- PromiseKit https://github.com/mxcl/PromiseKit
Scala
- Scalaz Task - 缺失的文档 http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/
- Monix https://monix.io/docs/2x/eval/task.html
Objective-C
Java
C++
- Folly futures https://github.com/facebook/folly/tree/master/folly/futures https://code.facebook.com/posts/1661982097368498/futures-for-c-11-at-facebook/
JavaScript
- Pied Piper https://github.com/WeltN24/PiedPiper/blob/master/README.md#promises
- Data.Task https://github.com/folktale/data.task
- fun-task https://github.com/rpominov/fun-task/blob/master/docs/api-reference.md#taskmaprejectedfn
其他灵感
- Monix 设计历史 https://gist.github.com/alexandru/55a6038c2fe61025d555
- 与 Task 相比,Future 是一个毫无价值的抽象吗? https://www.reddit.com/r/scala/comments/3zofjl/why_is_future_totally_unusable/ Monix 作者的有趣评论。
- 用 Scalaz 获得高效性能 - http://blog.higher-order.com/blog/2015/06/18/easy-performance-wins-with-scalaz/
- 引用透明度: https://wiki.haskell.org/Referential_transparency
- Promise 和 Task 的区别 https://glebbahmutov.com/blog/difference-between-promise-and-task/
- Future 和 Task 的区别 https://github.com/indyscala/scalaz-task-intro/blob/master/presentation.md
- 执行上下文 https://www.cocoawithlove.com/blog/specifying-execution-contexts.html
- ScalaZ Task:缺少的文档 http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/
- 比较不同语言的 Promises 框架 http://blog.slaks.net/2015-01-08/comparing-different-languages-promises-frameworks/
- Futures 和 Promises(主要针对实现列表) https://en.wikipedia.org/wiki/Futures_and_promises
设计决策
这些决策可能存在争议,但却是深思熟虑的。
- 不允许
success:failure:
和completion:
方法的连锁调用。读者可能会轻易误解连锁操作将按顺序执行。 - 不公开
success:
方法或failure:
方法。我们认为,让任何副作用明确表明它们不处理值或不想处理错误,通过传递一个NULL
参数,是一个更好的做法。 - 不要实现 BrightFutures 的行为:“如果通过 Main 注册,则在 Main 执行回调,否则在后台执行回调”。我们认为显式执行器更好。在使用 BrightFuture 的行为时,复制到另一个位置的代码可能会由于非常微妙的原因而表现不当。
- 不要将
value
和error
传递给completion
块。如果调用者需要消费value
或error
,他们应该使用success:failure:
。如果需要执行不需要消费值的清理代码,则使用completion
更合适。如果将value
和error
传递给completion
,回调代码非常容易误解未来是否已解析或拒绝。
作者
许可证
版权所有 2016-2018 Pinterest, Inc
Apache License, Version 2.0 许可协议下发布;“许可证”;除非符合适用的法律或书面同意,否则不得使用此文件。您可以在https://apache.ac.cn/licenses/LICENSE-2.0 获取许可证副本
除非法律要求或书面同意,在许可证下分发的软件按“现状”提供,不提供任何明示或暗示的保证或条件。有关许可证项下权限和限制的特定语言,请参阅许可证。