本书译者为: kevinHM
ReactiveCocoa的实践
在这一章中我们将首次运用ReactiveCocoa进行实际应用的编写,主要是创建一个叫做'500px'的简单应用。'500px'类似于'Flickr',存放在里面的都是些得到你认可的照片,使用'500px'的API的原因有两点:
- 照片看起来非常棒
- 当我还在那里工作的时候,我为他们的API接口写了iOS的SDK,我很熟悉她。
这一章我们分三个部分来讲解:
- 首先将完成我们的App(FunctionalReactivePixels)的基本实现。
- 其次我们将添加一些新的视图控制器,做更多的数据加载,来进一步证实第一步的实现。
- 最后我们将重新审视这个应用程序,以消除更多的状态获取使用更多函数响应型编程的机会。
这一章非常有趣,当然由我亲笔完成。我们应用程序'FunctionalReactivePixels'最后的结果开源在Github上,不幸的是,创作这个App的中间一些过程并不会展现在最后的结果里,但是如果你一章一章跟着我来的话,应该会很好。
FunctionalReactivePixels基础
FunctionReactivePixels将会是一个简单的观看'500px'中最受欢迎的照片的应用。一旦我们完成这一节,应用的主界面将会像下面这样:
当然我们也可以像下图一样观看全屏模式下的图片。 这个App将使用Collection Views。如果你没有太多这方面的经验,也不需要太过担心---他们(CollectionView)就像TableView一样,使用起来非常简单。如果你对UICollectionView感兴趣,可以阅读我的另一本书.我们将使用CocoaPods来管理我们的依赖,现在创建一个新的工程。我喜欢使用空模版以便我可以完全控制viewController层级。
首先、我们将创建一个UICollectionViewController的子类FRPGalleryViewController.同时我们创建一个UICollectionViewFlowLayout的子类FRPGalleryFlowLayout.
#import the new flow layout's header in the view controller's implementation file and
#then override FRPGalleryViewController's init method
- (id)init{
FRPGalleryFlowLayout *flowLayout = [[FRPGalleryFlowLayout alloc] init];
self = [self initWithCollectionViewLayout:flowLayout];
if(!self) return nil;
return self;
}
这将初始化collection View的layout为我们自己的layout.这个flowlayout子类的实现非常简单,只需要设置一些属性就可以了。
@implementation FRPGalleryFlowLayout
- (instancetype)init{
if (!(self = [super init])) return nil;
self.itemSize = CGSizeMake(145,145);
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.sectionInset = UIEdgeInsetsMake(10,10,10,10);
return self;
}
@end
很棒!下一步,我们需要把Viewcontroller展现在屏幕上。为了实现这个,我们首先要在应用的application delegate的application: didFinishLaunchingWithOptions:方法。我们想要将collectionview Controller置于一个navigationController容器中:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[FRPGalleryViewController alloc] init]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
很好!如果我们现在运行,我们将看到一个空视图。
我们来填充一些内容。创建一个Podfile文件,并填写如下内容:
platform :ios, "7.0"
target "FRP" do
pod 'ReactiveCocoa', '~> 2.1.4'
pod 'libextobjc', '~> 0.3'
pod '500-iOS-api', '~> 1.0.4'
pod 'SVProgressHUD', '~> 0.9'
end
target "FRPTests" do
end
下一章,我们将添加一些测试。现在运行pod install,然后打开Xcode通用的workspace文件。打开与编译头文件FRP-Prefix.pch(Xcode6之后,新建工程默认不加载pch文件,需要自己添加,Apple的最佳实践中已经不推荐使用全局的预编译pch文件),然后添加下面的内容。这些语义会自动加载到项目的所有文件中。
//Pods
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <500px-iOS-api/PXAPI.h>
#import <libextobjc/EXTScope.h>
//App Delegate
#import "FRPAppDelegate.h"
#define AppDelegate ((FRPAppDelegate *)[[UIApplication sharedApplication] delegate])
对于这样使用AppDelegate单例的用法,Saul Mora说:“每次看到你这么做,我家的狗都想死”。 但是这不是一本关于设计模式的书---这是一本关于ReactiveCocoa的书,所以我们可能要害死一些狗狗。。。
创建一个AppDelegate的属性来hold住500px API客户端
@property (nonatomic, readonly) PXAPIHelper * apiHelper; 在application:didFinishLaunchingWithOptions:方法中实例化这个变量。
self.apiHelper = [[PXAPIHelper alloc]
initWithHost:nil
consumerKey:@"DC2To2BS0ic1ChKDK15d44M42YHf9gbUJgdFoF0m"
consumerSecret:@"i8WL4chWoZ4kw9fh3jzHK7XzTer1y5tUNvsTFNnB"];
我提供了一对一次性消费的密钥---请不要疯到你也使用这对密钥,你可以申请自己的。
好了,我们差不多也该建立数据的加载了。我们需要一个数据模型来hold住我们的信息。我创建了下面的FRPPhotoModel。
@interface FRPPhotoModel : NSObject
@property (nonatomic, strong) NSString *photoName;
@property (nonatomic, Strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *photographerName;
@property (nonatomic, strong) NSNumber *rating;
@property (nonatomic, strong) NSString *thumbnailURL;
@property (nonatomic, strong) NSData *thumbnailData;
@property (nonatomic, strong) NSString *fullsizedURL;
@property (nonatomic, strong) NSData * fullsizedData;
@end
@implementation FRPPhotoModel
@end
非常好,到这里,我们将不直接在ViewController中加载内容,相反,这部分逻辑将被抽象到另一个类中。创建一个名为FRPPhotoImporter的类。
到现在为止没有一处代码是关于函数式的。别担心,我们就要这么做了!这个FRPPhotoImporter将不会真正返回一个FRPPhotoModel对象,相反他会返回一些随身携带API最新的请求结果的信号。
@interface FRPPhotoImporter : NSObject
+ (RACSignal *)importPhotos;
@end
FRPPhotoImporter的importPhotos方法返回一个从API发送最新结果的RACSignal。这个RACSignal实际上是一个RACReplaySubject.但是由于ReactiveCocoa编程指南中不建议使用RACSubjects,我们申明的公共接口的返回类型为RACSignal而非RACSubject.现在让我们继续往下看:
+ (RACSignal *)importPhotos{
RACReplaySubject * subject = [RACReplaySubject subject];
NSURLRequest * request = [self popularURLRequest];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError){
if (data) {
id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
[subject sendNext:[[[results[@"photos"] rac_sequence] map:^id(NSDictionary *photoDictionary){
FRPPhotoModel * model = [FRPPhotoModel new];
[self configurePhotoModel:model withDictionary:photoDictionary];
[self downloadThumbnailForPhotoModel:model];
return model;
}] array]];
[subject sendCompleted];
}
else{
[subject sendError:connectionError];
}
}];
return subject;
}
这里面包含的内容太多,我们慢慢来整理一下:
- 首先我们创建了一个新的RACReplaySubject实例(这将是我们要返回的对象)。
- 其次我们创建了一个NSURLRequest来获取500px上热门的FRPPhotoModel数据。
- 随后我们发送一个网络的异步请求,并立即返回RACSubject对象。 这个直接返回的结果值得我们关注。
这个RACSubject对象被异步网络请求的回调block捕获,当API接口返回数据时回调block就会被调用,然后RACSubject对象会将结果传送出来,这些值将被我们的订阅了RACSubject信号的接收者所接受。
这是你看到的异步操作中,一个非常普通的模式。
- 创建一个RACSubject.
- 从异步调用的完成block中向RACSubject传送结果值。
- 立即返回这个RACSubject对象 重要的是,要注意一个普通的RASSubject及其子类RACReplaySubject之间的区别。RACReplaySubject可以确保他背后的Subject只会被订阅一次,避免执行重复的操作(就像上面这种网络活动的情况),RACReplaySubject将会缓存这个订阅的值,并将其转发给新的订阅者们--- 对我们的需求来说这非常完美。就像ReactiveCocoa的开发者Justin Spahr-Summers所指出的,这也能够避免可能的竞争状况。
我们发送了一个完整的数据集而不是单个随时间变化的流。如果我们连环地发送一个个单独的FRPPhotoModel流,这将'更加Reactive',也有助于实现分页的需求,但是我们不打算采用这种方式,因为他有点点‘高级’了。你可以下载octokit:一个类似这种方式的例子。
URL请求的构造方法看起来应该是这样的:
+ (NSURLRequest *)popularURLRequest {
return [AppDelegate.apiHelper urlRequestForPhotoFeature:PXAPIHelperPhotoFeaturePopular
resultsPerPage:100 page:0
photoSize:PXPhotoModelSizeThumbnail
sortOrder:PXAPIHelperSortOrderRating
except:PXPhotoModelCategoryNude];
}
subject发送什么,完全看不到好吗?呃。这取决于回调block.
if(data){
id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
[subject sendNext:[[[results[@"photos"] rac_sequence] map:^id (NSDictionary *photoDictionary){
FRPPhotoModel *model = [FRPPhotoModel new];
[self donwloadThumbnailForPhotoModel:model];
return model;
}] array]];
[subject sendCompleted];
}
else{
[subject sendError:connectionError];
}
测试是否有数据返回时,可以说这不是一个很好的错误条件检测的方法,但这是一个教学的例子。如果数据为nil,我们会发送一个errorValue,否则我们会反序列化JSON数据并处理它。这不太容易很快就看清楚是怎么做到的,让我们来仔细看看。
[subject sendNext:[[[results[@"photos"] rac_sequence] map:^id (NSDictionary *photoDictionary){
FRPPhotoModel * model = [FRPPhotoModel new];
[self configurePhotoModel:model withDictionary:photoDictionary];
[self downloadThumbnailForPhotoModel:model];
return model;
}] array]];
[subject sendCompleted];
发送一个值,随着subject撸过去,第一个表达式结构相当简洁(但是场景很典型)。这个值是photos的值,然后转化为一个序列(sequence),然后做映射,最后转化为一个数组。这是上一章介绍的非常简单的map技术。
这个map(映射)非常有意思。序列中的每一个元素,都会创建一个新的FRPPhotoModel对象、设置它然后返回它。为每一个results[ @"photos" ]的数组元素创建了一个FRPPhotoModel数组。这个数组就是随着subject发送过来的值。最后我们发送一个完成值completedValue好让订阅者们知道任务完成了。
注意在信号上手动附送值的能力是非典型的,这是RACSubject实例的专属能力。
configurePhotoModel:withDictionary:方法,看起来应该像下面这样:
+ (void)configurePhotoModel:(FRPPhotoModel *)photomodel withDictionary:(NSDictionary *)dictionary{
//Basic details fetched with the first, basic request
photomodel.photoname = dictionary[@"name"];
photomodel.identifier = dictionary[@"id"];
photomodel.photographerName = dictionary[@"user"][@"username"];
photomodel.rating = dictionary[@"rating"];
photomodel.thumbnailURL = [self urlForImageSize:3 inArray:dictionary[@"images"]];
//Extended attributes fetched with subsequent request
if (dictionary[@"comments_count"]){
photomodel.fullsizedURL = [self urlForImageSize:4 inArray:dictionary[@"images"]];
}
}
除了URL的属性设置,都是最基本的东西。依靠其他的方法来从500px的API中返回的图片列表中提取正确的url信息。500px API返回的数据结构是下面这样的格式:
(
{
size = size;
url = ...;
}
)
这是一个字典数组,每一个字典中包含一个size字段和一个url字段。我们读取这样字段的方法如下:
+ (NSString *)urlForImageSize:(NSInteger)size inDictionary:(NSArray *)array{
return [[[[[array rac_sequence] filter:^ BOOL (NSDictionary * value){
return [value[@"size"] integerValue] == size;
}] map:^id (id value){
return value[@"url"];
}] array] firstObject];
}
这里有一些隐含的错误处理,如果序列为空,NSArray的firstObject方法默认返回nil.
- 第一步,我们过滤掉那些size字段不匹配要求的字典。
- 然后,将这些符合要求的字典做一次映射来提取字典中url字段的内容。
- 最后,我们获得一个NSString 对象的序列,把它转化为数组,然后返回firstObject.
在ReactiveCocoa中类似上面的链式调用非常常见。值从rac_sequence推送到filter:方法中,最后推送到map:方法里。最后调用序列rac_sequence的array方法,将序列的结果转化为array.
最后,我们的downloadThumbnailForPhotoModel:方法,看起来应该是下面这样:
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel{
NSAssert(photoModel.thumbnailURL, @"Thumbnail URL must not be nil");
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:photoModel.ThumbnailURL]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError * connectionError){
photoModel.thumbnailData = data;
}];
}
这个方法里面没有任何的关于Reactive的部分---仅仅是下载thumbnail的url,然后在完成块中适当地设置相关属性。
我们几乎做完了这个画廊所需要的所有基础的事情,接下来,我们看看viewController.在实现文件里定义下面的的私有属性。
@interface FRPGalleryViewController ()
@property (nonatomic , strong) NSArray *photoArray;
@end
来看下viewDidLoad中的实现。
static NSString * CellIdentifier = @"Cell";
- (void)viewDidLoad{
[super ViewDidLoad];
//Configure self
self.title = @"Popular on 500px";
//Configure View
[self.collectionView registerClass:[FRPCell class] forCellWithReuseIdentifier:CellIdentifier];
//Reactive Stuff
@weakify(self);
[RACObserver(self, photosArray) subscribeNext:^(id x){
@strongify(self);
[self.collectionView reloadData];
}];
//Load data
[self loadPopularPhotos];
}
我们为viewController设置了一个title并且为collectionView注册了一个类,collectionView将会在他的cells中复用这个类的实例。这里我引用了一个不存在的UICollectionViewCell的子类,我们很快会创建她。
在'Reactive Stuff'注释之下,你会发现一些奇怪的语法。
@weakify(self);
[RACObserver(self, photosArray) subscribeNext:^(id x){
@strongify(self);
[self.collectionView reloadData];
}];
RACObserver
是一个C的宏定义,带两个参数:对象及对象某个属性的keyPath(关键路径)。他会返回一个带属性值的信号,无论这个属性的值怎么变都会及时地通过该信号反馈出来。在这里当self结束分配的时候会发送一个completion Value的值。订阅这个信号的目的是无论我们的photosArray中的元素属性怎么变,我们都能够在collectionView重新加载的时候实时获取反馈。
在Objective-C的ARC条件下@weakify/@strongify这个双人舞是非常常见的。@weakify创建一个新的self的弱引用weakself,@strongify创建这个weakself的强引用,并在@strongify的作用域中起作用。strongify的这种做法,一般称为“影子变量”,那是因为这个新的强引用的变量就叫self,替代了原本强引用的self.
一般而言,subscribeNext:的block将捕获其词法范围内的self,造成self和block之间的循环引用。block被subscribeNext:的返回值,一个RACSubscriber实例,强引用,然后被RACObserver宏捕获。解除分配时,RACOberver会自动解除第一个参数的分配,这样的话self就应该被解除分配,但self被block强引用,self要得以解除分配的唯一条件即引用计数为0,这样的话就必须先解除block的分配,而前面的分析我们知道block被RACSubscriber实例引用,而该实例默认被self强引用,因此,如果不调用weakify/strongify,self就永远也不可能解除分配。
最后,我们实际来调用loadPopularPhotos(他的实现如下)
- (void)loadPopularPhotos{
[[FRPPhotoImporter importPhotos] subscribeNext:^(id x){
self.photosArray = x;
} error:^(NSError * error){
NSLog(@"Couldn't fetch photofrom 500px: %@",error);
}];
}
这个方法实际上负责调用FRPPhotoImporter的importPhotos方法(现在请加上他的头文件),他订阅了我们私有成员属性的结果。由于UICollectionViewDataSource协议的架构,我们不得不把这些状态引入进来。
现在让我们来看一下这些协议方法,有两个是必须的,实现如下:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.photosArray.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
FRPCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
[cell setPhotoModel:self.photosArray[indexPath.row]];
return cell;
}
第一个方法简单地返回了collectionView中的cell的数量,在这里,准确地讲是photosArray属性的cell数量。接下来的这个方法从collectionView列表中获得了一个cell实例,并调用其上的setPhotoModel:方法(这个我们还没有实现,但别担心)。这些代码应该看起来非常熟悉,如果你曾经处理过UITableViewDataSource的方法的话。
这就是我们ViewController完整的实现。现在我们来创建UICollectionViewCell的子类,命名为FRPCell,像下面这样来修改他的头文件。
@class FRPPhotoModel;
@interface FRPCell : UICollectionViewCell
- (void)setPhotoModel:(FRPPhotoModel *)photoModel;
@end
在实现文件中添加下面的私有扩展:
#import "FRPPhotoModel.h"
@interface FRPCell ()
@property (nonatomic , weak ) UIImageView * imageView;
@property (nonatomic , strong ) RACDisposeable *subscription;
@end
这里有两个属性:一个图片视图和一个订阅者。图片视图是弱引用,因为它属于父视图(这是UICollectionViewCell的一个标准的用法),我们将实例化并赋值给imageView。接下来的属性是一个订阅,当使用ReactiveCocoa来设置图像视图的图像属性时,我们将接触到它。注意它必须是强引用而非弱引用否则你会得到一个运行时的异常。
- (id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(!self) return nil;
//Configure self
self.backgroundColor = []UIColor darkGrayColor];
//Configure subviews
UIImageView * imageView = [[UIImageView alloc] initWithFrame:self.bounds];
imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.contentView addsubView:imageView];
self.imageView = imageView;
return self;
}
标准的UICollectionView子类的模版会创建并分配imageView属性。注意,我们必须有一个(被self)强引用的本地变量作为中介来存储imageView,这样就不会在赋值给self的imageView属性的时候,imageView被立即解除分配。否则会有编译错误。
完成我们的500px画廊,我们还需要实现两个方法,第一个就是setPhotoModel:方法
- (void)setPhotoModel:(FRPPhotoModel *)photoModel{
self.subscription = [[[RACObserver(photoModel, thumbnailData)
filter:^ BOOL (id value){
return value != nil;
}] map:^id (id value){
return [UIImage imageWithData:value];
}] setKeyPath:@keypath(self.imageView, image) onObject:self.imageView];
}
这种方法来给订阅的属性赋值,我们老早就知道了。它把setKeyPath:OnObject:的返回值赋给了self.subscription.实践中这种方法根本不使用,我们使用RAC的C语法宏来代替,不久之后我们就会涉及这方面的知识。
两个原因导致订阅是必要的:
1. 当它没有接受一个新的值时,我们想延迟处理。
2. 信号的订阅通常是冷信号,除非有人订阅他(信号),否则信号不会起作用。
setKeyPath:onObject:是RACSignal的一个方法:绑定最新的信号的值给对象的关键路径。在这里我们在一个级联的信号上调用了这个方法,让我们来仔细看看:
[[RACObserver (photoModel, thumbnailData)
filter:^BOOL (id value){
return value != nil;
}] map:^ id (id value){
return [UIImage imageWithData:value];
}];
信号由RACObserver这个C的宏生成,这个宏简单地返回一个监控目标对象关键路径值变化的信号。在我们这个例子中,我们的目标对象是photoModel,关键路径为thumbnailData属性。我们过滤掉所有的nil值,然后对过滤后的值做映射:把NSData实例转为UIImage对象。
注意,把NSData实例转化为UIImage的这个映射仅在小图上可以很好地运行,如果频繁地做这个映射或者作用到大图上会引起性能问题。理想的情况下,我们会缓存这些已经解压的图像以避免每一次都重复计算。这个技术不是本书所讨论的范畴,但我们将使用另一个通过ReactiveCocoa来实现的方法。
thumbnailData属性根本不需要在这里设置,他可以在稍后的某个时间在应用的其他部分来完成设置,然后cell的图像就会像魔术一般更新。
可以让我们稍微突破一下Model-View-Controller模式好吗?只是一点点的不守规矩。幸运的是,下一章我们将看到无处不在的MVC模式的困境,所以我们不必担心这一点点的突破,一点点的改进。
上面提到的setKeyPath:onObject:方法中,一旦onObject:对象被释放,他的订阅也会被自动取消。我们的cell实例是被collectionView所复用的,因此在复用的时候,我们需要取消cell上各组件的订阅。我们可以通过重写UICollectionViewCell的下列方法达成:
- (void)perpareForReuse {
[super prepareForReuse];
[self.subscription dispose], self.subscription = nil;
}
这个方法在Cell被复用之前调用。如果现在运行我的应用,我们可以看到下面的结果:
太好了!我们可以通过滚动视图来证实我们手动处理订阅的有效性。
添加FunctionalReactivePixels
当我们弄好了一个简单的画廊之后,就会想要在画廊中查看高清图片了。当用户点击画廊中的某一个单元格时,我们创建一个新的视图控制器并将其推入到导航堆栈中。
- (void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
FRPFullSizePhotoViewController * viewController = [[FRPFullSizePhotoViewController alloc] initWithPhotoModels:self.photosArray currentPhotoIndex:indexPath.item];
viewController.delegate = self;
[self.navigationController pushViewController:viewController animated:YES];
}
这个方法没有任何特殊的,只是些一般的OC方法。当然别忘了在当前实现文件里加载视图控制器(FRPFullSizePhotoViewControler)的头文件.现在让我们来创建这个视图控制器(FRPFullSizePhotoViewControler).
创建一个UIViewController的子类FRPFullSizePhotoViewControler,这不会是一个特别的‘Reactive’的视图控制器,实际上大部分只是UIPageViewController子视图控制器的模版。
@class FRPFullSizePhotoViewController;
@protocol FRPFullSizePhotoViewControllerDelegate <NSOject>
- (void)userDidScroll:(FRPFullSizePhotoViewController *)viewController toPhotoAtIndex:(NSInteger)index;
@end
@interface FRPFullSizePhotoViewController : UIViewController
- (instancetype)initWithPhotoModels:(NSArray *)photoModelArray currentPhotoIndex:(NSInteger)photoIndex;
@property (nonatomic , readonly) NSArray *photoModelArray;
@property (nonatomic, weak) id<FRPFullSizePhotoViewControllerDelegate> delegate;
@end
回到画廊视图控制器实现必要的代理方法:
- (void)userDidScroll:(FRPFullSizePhotoViewController *)viewController toPhotoAtIndex:(NSInteger)index{
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
}
当我们滑到一个新的图像去查看其高清图片时,这个方法将更新collectionView滑动的位置。这样一来,当用户查看完高清图回到这个界面的时候,高清图所对应的缩略图将会显示在界面上,方便用户获知自己浏览的位置以及继续往下浏览。
#import
这些必要的数据模型的头文件并追加一下两个私有属性:
@interface FRPFullSizePhotoViewController () <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
//Private assignment
@property (nonatomic, strong) NSArray *photoModeArray;
//Private properties
@property (nonatomic, strong) UIPageViewController *pageViewController;
@end
photoModelArray是共有的只读属性,但是内部可读写。第二个属性是我们的子视图控制器。我们这样来初始化:
- (instancetype)initWithPhotoModels:(NSArray *)photoModelArray currentPhotoIndex:(NSInteger)photoIndex{
self = [self init];
if (!self) return nil;
//Initialized, read-only properties
self.photoModelArray = photoModelArray;
//Configure self
self.title = [self.photoModelArray[photoIndex] photoName];
//ViewControllers
self.pageViewController = [UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControlerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:@{ UIPageViewControllerInterPageSpacingKey: @(30)};
self.pageViewController.dataSource = self;
self.pageViewController.delegate = self;
[self addchildViewController:self.pageViewController];
[self.pageViewController setViewController:@[[self photoViewControllerForIndex:photoIndex]]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO completion:nil ];
return self;
}
赋值属性、设置标题、配置我们的pageViewController,一切都非常无聊,我们的viewDidLoad方法也同样简单。
- (void)viewDidLoad{
[super viewDidLoad];
self.view,backGroundColor = [UIColor blackColor];
self.pageViewController.view.frame = self.view.bounds;
[self.view addSubView:self.pageViewController.view];
}
我要指出的是,简便起见,在我的应用里我禁用了横向展示,因为这不是一本关于autoresizingMask或者autoLayout的书。你可以通过Eria Sadun的书了解更多关于autoLayout方面的细节。
下面我们来了解一下UIPageViewController的数据源协议和代理协议。
- (void)pageViewController:(UIPageViewController *)pageViewController
didFinishAnimating: (BOOL)finished
previousViewControllers:(NSArray *)previousViewControllers
transitionCompleted:(BOOL)completed{
self.title = [[self.pageViewController.viewControllers.firstObject photoModel] photoName];
[self.delegate userDidScroll:self toPhotoAtIndex:[self.pageViewController.viewControllers.firstObject photoIndex]];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(FRPPhotoViewController *)viewController{
return [self photoViewControllerForIndex:viewController.photoIndex - 1];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(FRPPhotoViewController *)viewController {
return [self photoViewControllerForIndex:viewController.photoIndex + 1];
}
虽然这些方法没有技术上的reactive,却体现出一定意义上的实用性。我很佩服这种在特殊类型的视图控制器上的抽像,干得漂亮,Apple!
我们的视图控制器创建方法,类似下面这样:
- (FRPPhotoViewController *)photoViewControllerForIndex:(NSInteger)index{
if (index >= 0 && index < self.photoModelArray.count){
FRPPhotoModel *photoModel = self.photoModelArray[index];
FRPPhotoViewController *photoViewController = [[FRPPhotoViewController alloc] initWithPhotoModel:photoModel index:index];
return photoViewController;
}
//Index was out of bounds, return nil
return nil;
}
它基本上创建比配置了一个我们将要使用的UIViewController的子视图控制器FRPPhotoViewController。下面是他的头文件:
@class FRPPhotoModel;
@interface FRPPhotoViewController : UIViewController
- (instancetype)initWithPhotoModel:(FRPPhotoModel *)photoModel index:(NSInteger)photoIndex;
@property (nonatomic, readonly) NSInteger photoIndex;
@property (nonatomic, readonly) FRPPhotoModel * photoModel;
@end
这个视图控制器非常简单:显示一个photoModel下的高清图片,并提示photoImporter(单例对象)下载这个图片。它是如此简单,我现在就告诉你它的全部实现。
//Model
#import "FRPPhotoModel.h"
//Utilities
#import "FRPPhotoImporter.h"
#import <SVProgressHUD.h>
@interface FRPPhotoViewController ()
//Private assignment
@property (nonatomic, assign) NSInteger photoIndex;
@property (nonatomic, strong) FRPPhotoModel *photoModel;
//Private properties
@property (nonatomic, weak) UIImageView * imageView;
@end
@implementation FRPPhotoViewController
- (instancetype)initWithPhotoModel:(FRPPhotoModel *)photoModel index:(NSInteger)photoIndex{
self = [self init];
if (!self) return nil;
self.photoModel = photoModel;
self.photoIndex = photoIndex;
return self;
}
- (void)viewDidLoad{
[super viewDidLoad];
//Configure self's view
self.view.backGroundColor = [UIColor blackColor];
//Configure subViews
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
RAC(imageView, image) = [RACObserve(self.photoModel, fullsizeData) map:^id (id value){
return [UIImage imageWithData:value];
}];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubView:imageView];
self.imageView = imageView;
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[SVProgressHUD show];
//Fetch data
[[FRPPhotoImporter fetchPhotoDetails:self.photoModel]
subscribeError:^(NSError *error){
[SVProgressHUD showErrorWithStatus:@"Error"];
}
completed:^{
[SVProgressHUD dismiss];
}];
}
@end
就像我们的collectionViewCell中那样,我们将UIImageView的image属性和数据模型的某个属性映射后的值绑定,所不同的是ViewController不需要考虑复用,所以我们不必计较怎么取消imageView的订阅---当imageView对象解除分配的时候,订阅将会被取消。
这个实现里面另一个有趣的部分在viewWillAppear:里:
[SVProgressHUD show];
//Fetch data
[[FRPPhotoImporter fetchPhotoDetails:self.photoModel]
subscribeError:^(NSError * error){
[SVProgressHUD showErrorWithStatus:@"Error"];
}
completed:^{
[SVProgressHUD dismiss];
}];
没有收到错误或者完成信息之前,我们必须给用户展示网络请求的状态。你看,500px的受欢迎的照片的API接口只返回了一个照片的大概信息,但我们需要这个照片更详细的信息,所以我们必须调用第二个API接口来获取每一个照片的详细信息(包括全尺寸照片的URL)。
+ (NSURLRequest *)photoURLRequest:(FRPPhotoModel *)photoModel{
return [AppDelegate.apiHelper urlRequestForPhotoID:photoModel.identifier.integerValue];
}
我们还没有实现fetchPhotoDetails:方法,所以现在我们回到FRPPhotoImporter中,在头文件中定义这个方法,在实现文件中实现它。
+ (RACReplaySubject *)fetchPhotoDetails:(FRPPhotoModel *)photoModel {
RACReplaySubject * subject = [RACReplaySubject subject];
NSURLRequest *request = [self photoURLRequest:photoModel];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^ (NSURLResponse *response, NSData * data, NSError *connectionError){
if(data){
id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil][ @"photo" ];
[self configurePhotoModel:photoModel withDictionary:results];
[self downloadFullsizedImageForPhotoModel:photoModel];
[subject sendNext:photoModel];
[subject sendCompleted];
}
else{
[subject sendError:connectionError];
}
}];
return subject;
}
这种方法跟前面我们看到的importPhotos方法模式一样,我们的downloadFullsizedImageForPhotoModel:方法跟downloadThumbnailForPhotoModel:方法也是一样的。除了这两者之外,还有什么重要的抽象方法呢?让我们来完成我们的缩略图方法。
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
[self download:photoModel.thumbnailURL withCompletion:^(NSData *data){
photoModel.thumbnailData = data;
}];
}
+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
[self download:photoModel.fullsizedURL withCompletion:^(NSData * data){
photoModel.fullsizedData = data;
}];
}
+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
[self download:photoModel.fullsizedURL withCompletion:^(NSData *data){
photoModel.fullsizedData = data;
}];
}
+ (void)download:(NSString *)urlString withCompletion:(void(^)(NSData * data))completion{
NSAssert(urlString, @"URL must not be nil" );
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError){
if (completion){
completion(data);
}
}];
}
我曾经与这样一位客户工作过,他认为如果你某行一样的代码重复写两次,这代码就应该得到某种程度的抽象。虽然我认为这有点偏激,但我喜欢这种态度。
好了。我们现在可以运行这个应用,点击一个图片去查看它的高清图片。我们也可以向前或者向后滑动来查看前一个或后一个高清图片。非常棒!
和FunctionalReactivePixels一起实践
上一节,我们很多次使用了ReactiveCocoa的关键部分,这里有更多的机会来使用ReactiveCocoa整个代码库。开始吧!
首先在我们的画廊视图控制器中实现三个不同的代理方法:CollectionViewDataSource、CollectionViewDelegate、高清图视图控制器的PhotoViewControllerDelegate
使用一个称之为RACDelegateProxy的实例,我们可以抽象委托类型的协议的任何方法实现(比如:那些返回void类型的)。
委托代理是一个称为rac_signalForSelector:对象的‘白板’,获取当Selector被调用时发送的新值的信号。
注意:你必须retain这个delegate对象,否则他们将会被释放,你将会得到一个EXC_BAD_ACCESS异常。添加下列私有属性到画廊视图控制器:
@property (nonatomic, strong) id collectionViewDelegate;
同时你也需要导入RACDelegateProxy.h,因为他不是ReactiveCocoa的核心部分,不包含在ReactiveCocoa.h中。移除UICollectionViewDelegate以及FRPFullsizePhotoViewControllerDelegate方法,追加下面的代码到viewDidLoad.
RACDelegateProxy *viewControllerDelegate = [[RACDelegateProxy alloc]
initWithProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)];
[[viewControllerDelegate rac_signalForSelector:@selector(userDidScroll:toPhotoAtIndex:) fromProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)]
subscribeNext:^(RACTuple *value){
@strongify(self);
[self.collectionView
scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[value.second integerValue] inSection:0]
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
}];
self.collectionViewDelegate = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UICollectionViewDelegate)];
[[self.collectionViewDelegate rac_signalForSelector:@selector(collectionView:didSelectItemAtIndexPath:)]
subscribeNext:^(RACTuple *arguments) {
@strongify(self);
FRPFullSizePhotoViewController *viewController = [[FRPFullSizePhotoViewController alloc] initWithPhotoModels:self.photosArray currentPhotoIndex:[(NSIndexPath *)arguments.second item]];
viewController.delegate = (id<FRPFullSizePhotoViewControllerDelegate>)viewControllerDelegate;
[self.navigationController pushViewController:viewController animated:YES];
}];
我们也可以在self上调用rac_signalForSelector:,使用同样的block块。然而,我们有必要在视图控制器实现里提供一个空存根方法以避免编译器发出"实现不完全"之类的警告。
空存根方法:源于C++的一个非常不错的函数设计方法。在设计整个程序时,一般会先编写完所有的代码,然后开始编译和测试,但这样有时候会出现一大堆错误而不知从哪里入手,这时我们可以采用空存根技术。
存根是一个仅仅返回某个意义不大的值的空函数。存根可以用来测试整个程序的逻辑关系,以及分块实现程序的不同部分。
设计一个程序时,先分析设计程序的各个函数完成的功能;然后直接设计函数的存根并编译,编译通过,证明程序的逻辑关系没有问题的情况下,再来分别实现各个不同的函数(存根)。
接下来,我们有更多的机会来抽象这个类中的方法。loadPopularPhotos方法除了改变我们的状态之外,并没有什么卵用。如果ReactiveCocoa能够很好地监控这些状态,让我们不在这方面担心的话,那肯定是极好的!幸运的是,我恰好知道这个~
我们移除这个方法,在viewDidLoad中键入下面的代码来代码这个方法的调用:
RACSignal *photoSignal = [FRPPhotoImporter importPhotos];
RACSignal *photosLoaded = [photoSignal catch:^RACSignal *(NSError *error) {
NSLog(@"Couldn't fetch photos from 500px : %@",error);
return [RACSignal empty];
}];
RAC(self, photosArray) = photosLoaded;
[photosLoaded subscribeCompleted: ^{
@strongify(self);
[self.conllectionView reloadData];
}];
一开始我们只是进行了importPhotos方法调用,不同的是,我们用signal来存放其返回值。 然后,我们“捕抓”这个信号上的错误并将它打印出来(跟我们之前做的一样,只不过语法不同而已)。比起subscribeError:方法,catch:方法处理的更为巧妙:它允许无错误值的信号穿透它,仅在信号有错误事件发生时才会调用它的block并发送其在发生错误时的返回值。这里我们使用catch:方法,来过滤无错误的值。这个catch:块仅仅返回一个空信号。更多关于这方面知识的细节请参考StackOverFlow的问题。
上面的方式,有一点点污染了我们的局部变量作用域,这可以用下面的更简洁的等效方法:
RAC(self, photosArray) = [[[[FRPPhotoImporter importPhotos]
doCompleted:^{
@strongify(self);
[self.collectionView reloadData];
}] logError] catchTo:[RACSignal empty]];
使用RAC宏,我们创建了photosLoaded信号的最新值到photoArray属性的单向绑定。太好了,保持状态!
我们来看一下,我们的collectionViewCell的子类实现:
@interface FRPCell ()
@property (nonatomic, weak) UIImageView *imageView;
@property (nonatomic, strong) RACDisposable *subscription;
@end
@implementation FRPCell
- (instancetype)initWithFrame:(CGRect)frame {
...
}
- (void)perpareForReuse {
[super perpareForReuse];
[self.subscription dispose], self.subscription = nil;
}
- (void)setPhotoModel:(FRPPhotoModel *)photoModel {
self.subscription = [[[RACObserve(photoModel, thumbnailData) filter:^BOOL(id value) {
return value != nil;
}] map:^id(id value) {
return [UIImage imageWithData:value];
}] setKeyPath:@keypath(self.imageView, image) onObject:self.imageView];
}
@end
这里有两个标志性的点表明了一个使用ReactiveCocoa来抽象的机会。
我们有状态(subscription属性) 我们手动处理RACDisposable的生命周期 无论何时调用一个RACDisposable对象的dispose方法,就是一个"这里有更加响应式的方法来作某件事"的好信号。在我们的例子中,这种嗅觉是对的。
通过在FRPCell创建一个新的属性,我们能够抽象掉使用prepareForReuse方法的必要性。这个属性就是photoModel(我们之前的行为就像是一个只写的属性,现在它将变为可读写的了)。把属性放在文件顶部:
@property (nonatomic, strong ) FRPPhotoModel *photoModel; 下一步我们将彻底摆脱setPhotoModel:方法。我们将为photoModel的thumbnailData观察我们自己的关键路径。将下面的代码添加到cell的初始化函数中。
RAC(self.imageView, image) = [[RACObserve(self, photoModel.thumbnailData) ignore:nil]
map:^(NSData *data){
return [UIImage imageWithData:data];
}];
注意看我们观察的是self
的photoModel.thumbnailData
的关键路径,而非self.photoModel
的thumbnailData
的关键路径。这点微妙的区别,作用却大大不同。当self
的属性photoModel
或者hotoModel
的thumbnailData
属性改变时,关键路径photoModel.thumbnailData
将会收到一个被(这种变化所)引发的KVO消息。
现在我们总算彻底摆脱了subscription
属性!
ReactiveCocoa网络层回访
还有一个机会来进一步接受我们函数反应型编程的理念,那就是我们的网络层 FRPPhotoImporter,我们先来看看下载图片的方法:
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
[self download:photoModel.thumbnailURL withCompletion:^(NSData *data) {
photoModel.thumbnailData = data;
}];
}
+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
[self download:photoModel.fullsizedURL withCompletion:^(NSData *data){
photoModel.fullsizedData = data;
}];
}
+ (void)download:(NSString *)urlString withCompletion:(void (^)(NSData *data))completion {
NSAssert(urlString, @"URL must not be nil");
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(completion) {
completion(data);
}
}];
}
Completion blocks?这是另外一个使用Signals的机会。更深入一点来说,我们可以使用NSURLConnection的ReactiveCocoa的扩展。下面我们来重写上面的方法:
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
RAC(photoModel, thumbnailData) = [self download:photoModel.thumbnailURL];
}
+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
RAC(photoModel,fullsizedData) = [self download:photoModel.fullsizedURL];
}
+ (RACSignal *)download:(NSString *)urlString {
NSAssert(urlString , @"URL must not be nil");
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString: urlString]];
return [[[NSURLConnection rac_sendAsynchronousRequest:request]
map:^id (RACTuple *value) {
return [value second];
}] deliverOn:[RACScheduler mainThreadScheduler]];
}
这里有两个大的不同:
- 我们使用RAC来绑定downloadFullsizedImageForPhotoModel:返回的信号的最新值。
- 我们返回NSURLConnection的rac_sendAsynchronousRequest:返回值的映射。
我们来看看这里究竟发生了什么。 看文档:rac_sendAsynchronousRequest:返回一个发送网络请求响应值的信号。RACTuple它所发送的内容分别包含响应和数据。有网络错误发生时,它会抛出错误。 最后我们改变线程的调度,将signal切换到主线程上。 (一个线程的调度者类似于一个线程。)
看,网络信号将会把它的值返回给后台的调度者,如果我们不阻止它,它可能最终会去从事更新UI的事件,而后台线程是没有能力更新UI的。
我们回过头来看看最开始的那两行。注意下这行:
RAC(photoModel, thumbnailData) = [self download:photoModel.thumbnailURL];
通常,我不推荐将一个model绑定到多个signal,然而,我们知道这个信号会在完成网络调用后立即执行完并结束订阅。只要我们仅在一个实例上绑定这个keyPath,这种就是安全的。
我们可以用类似的方式抽象掉使用RACReplaySubject的部分,来重新审视我们的fetchPhotoDetails:方法吧。
+ (RACReplaySubject *)fetchPhotoDetails:(FRPPhotoModel *)photoModel {
RACReplaySubject *subject = [RACReplaySubject subject];
NSURLRequest *request = [self photoURLRequest:photoModel];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(data) {
id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil][@"photo"];
[self configurePhotoModel:photoModel withDictionay:results];
[self downloadFullsizedImageForPhotoModel:photoModel];
[subject sendNext:photoModel];
[subject sendCompleted];
}
else {
[subject sendError:connectionError];
}
}];
return subject;
}
有一点点凌乱,我们来整理下。
+ (RACSignal *)fetchPhotoDetails:(FRPPhotoModel *)photoModel {
NSURLRequest *request = [self photoURLRequest:photoModel];
return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request]
map:^id(RACTuple *value){
return [value second];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
map:^id (NSData *data) {
id results = [NSJSONSerialization JSONObjectWithData:data
options:0 error:nil][@"photo"];
[self configurePhotoModel:photoModel withDictionary:results];
[self downloadFullsizedImageForPhotoModel:photoModel];
return photoModel;
}] publish] autoconnect];
}
注意: 返回值从RACReplaySubject *变成了RACSignal *. 这里有很多地方需要梳理,所以我们提前做了下面这个示意图来说明:
我们已经知道deliverOn:是怎样工作的,所以让我们来关注信号链条最末端的信号操作publish. publish返回一个RACMulitcastConnection,当信号连接上时,他将订阅该接收信号。autoconnect为我们做的是:当它返回的信号被订阅,连接到 该(订阅背后的)信号(underly signal)。
执行获取每一个订阅,在订阅的时候,我们返回的信号将会变“冷”。那是因为我们对底层信号进行多播,网络请求只会执行一次,但是它的结果被多播。这会导致:网络信号将只会被执行一次(当它被订阅时执行),是冷的(直到订阅为止,它不会被执行),甚至可删除的(如果一次性处理订阅的生成)。
基本上,我们能保证信号只会被订阅一次,我们不需要回滚(replay).
注意:我们可以用下面的reduceEach:替代使用RACTuple的第一个map:,以便提供编译时检查。
reduceEach:^id(NSURLResponse *response, NSData *data) {
return data;
}]
剩下的网络访问接口,importPhotos方法重构如下:
+ (RACSignal *)importPhotos {
NSURLRequest *request = [self popularURLRequest];
return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request]
reduceEach:^id(NSURLResponse *response , NSData *data){
return data;
}]
deliverOn:[RACScheduler mainThreadScheduler]]
map:^id (NSData *data) {
id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
return [[[results[@"photo"] rac_sequence]
map:^id (NSDictionary *photoDictionary) {
FRPPhotoModel *model = [FRPPhotoModel new];
[self configurePhotoModel:model withDictionary:photoDictionary];
[self downloadThumbnailForPhotoModel:model];
return model;
}] array];
}] publish] autoconnect];
}
总结ReactiveCocoa实践
本章我们使用ReactiveCocoa做了很多实践,总结了几个关键点:
-
函数式编程可在任何地方起作用
- 数据导入的代码,即使没有反应式代码,我们也能够使用map:和filter:来帮忙。在抽象方面,总觉得从未被实际实现。
-
为函数的副作用使用subscribeNext:
- subscribeNext:和其他类似的方法订阅信号的副作用,返回RACDisposable实例(这种实例将被传阅,直到信号完成被回收为止) 为副作用使用这些方法---使得事物看起来像主动跟外界(一个没有反应式的世界)交互似的。
-
避免显示状态下进行订阅处理
- 按照设计准则,无论何时都应该避免显示的订阅处理。请记住我们是怎样用takeUntil:来自动处理FRPCell类的订阅的。使用takeUntil:允许信号值通过,直到它的参数被传递下去或者它自己的值完成。基本上这种情况下,接收者已经完成接收了。
-
内存管理的魔法
- ARC下,在代码的表面上你摆脱了内存管理。ReactiveCocoa中也一样。唯一要注意的是,不能在任何signal的block中捕捉self。
以上,就是第五章的全部内容。接下来我们将介绍Model-View-ViewModel这种程序架构,给App添加一个日志系统,并写一些单元测试,出发吧!
BTY:函数副作用:指当调用函数时,除了返回函数值之外,还对主调用函数产生附加影响。例如修改全局变量或修改参数,一般而言函数副作用会给程序设计带来不必要的麻烦,使程序难以查找错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。
有一种特殊的情况,就是我们这里的函数。它的参数是一种In/Out作用的参数,即函数可能改变参数里面的内容,把一些信息通过输入参数,夹带到外界。这种情况,严格来说,也是副作用,是非纯函数。即我们所讨论的函数反应型编程中的函数式编程属于非纯函数,它是具有副作用的。