Block使用場景

西木柚子發表於2016-09-03

引言

最近在研究RAC的時候,發現絕大部分程式碼實現如下所示:

    RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {
        return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {
            return event.eventType == RACEventTypeCompleted;
        }] map:^id(id value) {
            return NSLocalizedString(@"Thanks", nil);
        }];
    }];複製程式碼

可以發現是block巢狀使用,這是使用block實現的函式程式設計正規化。

還有在使用masonry的時候,我們會見到如下程式碼:

[View mas_makeConstraints:^(MASConstraintMaker *make) {
      make.top.equalTo(anotherView);
      make.left.equalTo(anotherView);
      make.width.mas_equalTo(@60);
      make.height.mas_equalTo(@60);
}];複製程式碼

這裡使用的點語法連線,我們稱之為鏈式程式設計正規化。

而這些實現都是依靠block,所以這篇博文主要講解如下和block相關知識

  1. block作為引數
  2. block作為返回值
  3. block儲存程式碼塊
  4. block實現鏈式程式設計
  5. block實現函數語言程式設計

這篇博文需要你瞭解block的基礎知識,如果不瞭解,可以閱讀下面幾篇博文先做了解

IOS中 Block簡介與用法(一)

iOS深入學習(Block全面分析)

談Objective-C block的實現

一篇文章看懂iOS程式碼塊Block

其實塊就是OC中的匿名函式,無需定義函式名就可以使用,相當方便。具體看維基百科的定義(匿名函式


block作為引數

這應該是我們日常寫程式碼中接觸到最多的block使用場景了,我們通過AFN框架來看看。

1、在寫網路請求的時候我們經常會使用如下的程式碼:
        [[AFHTTPSessionManager manager]POST:@"http://www.baidu.com" parameters:params success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
            //返回響應成功後執行的程式碼塊1
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            //返回響應失敗後執行的程式碼塊2
        }
    ];複製程式碼
2、 而AFN框架對於該函式的實現如下

- (AFHTTPRequestOperation *)POST:(NSString *)URLString
                      parameters:(id)parameters
                         success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                         failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];

    [self.operationQueue addOperation:operation];

    return operation;
}複製程式碼
3、 上述函式繼續呼叫內部函式,把success和failure名字的block往下傳遞,直到如下函式,才執行這兩個block:
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                              failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{

        dispatch_async(http_request_operation_processing_queue(), ^{
            if (self.error) {
                if (failure) {
                    dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                        failure(self, self.error);
                    });
                }
            } else {
                id responseObject = self.responseObject;
                if (self.error) {
                    if (failure) {
                        dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                            failure(self, self.error);
                        });
                    }
                } else {
                    if (success) {
                        dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                            success(self, responseObject);
                        });
                    }

    };
}複製程式碼

我們在步驟1的時候,就把success block內實現的程式碼塊1和failure block內的程式碼塊2傳遞到了步驟2的函式,然後該函式在內部繼續呼叫內部方法,一層層把兩個程式碼塊傳遞到了步驟3,如下所示。


if (failure) {
                        dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                            failure(self, self.error);
                        });
                    }
                } else {
                    if (success) {
                        dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                            success(self, responseObject);
                        });
                    }複製程式碼

然後等待網路請求的回應失敗或者成功就呼叫相應的block,然後執行程式碼塊1或者程式碼塊2,如下所示

上述程式碼中的如下兩行程式碼,實現block的呼叫,並傳入相應的函式

failure(self, self.error);
success(self, responseObject);複製程式碼
總結:

通過上面的例子我們看到,先在block內部實現一個程式碼塊,因為block是一個OC物件,所以可以被當做引數傳遞到合適的地方,然後在合適的時候呼叫該block並傳入引數,就可以實現對該程式碼塊的呼叫,達到回撥的目的。

其實block就是一個物件,和OC中其他的物件一樣,所以可以被當做引數來傳遞,區別是block是一個匿名函式,所以你可以呼叫它實現某些功能。


block作為返回值

定義一個函式,讓block作為返回值,這樣就可以返回一個程式碼塊,然後在程式碼塊裡面執行某些操作完成一些功能。也可以返回自己,然後繼續呼叫該函式,返回一個block,這樣就可以實現masonry的鏈式呼叫效果,具體的我們下面再詳細講解。

先來看一個例子

#import 

@interface Car : NSObject

/**
 *  該函式返回一個block,該block無返回值,傳入的引數為int型別
 *  void:無返回值
 *  int: 引數型別為int
 */
-(void(^)(int))run;

/**
 *  該函式返回一個block,該block有返回值為NSString型別,傳入的引數為int型別
 *  NSString *:返回值為NSString型別
 *  int:       引數型別為int
 */
-(NSString*(^)(int ))drive;

@end複製程式碼

#import "Car.h"

@implementation Car

- (void (^)(int))run
{
    return ^(int meter){

        NSLog(@"car run %d meter",meter);
    };
}

-(NSString *(^)(int))drive{
    return  ^NSString *(int i){
        return [NSString stringWithFormat:@"I drive %zd meters in the car.",i];
    };
}

@end複製程式碼
呼叫上述方法
    Car *car = [[Car alloc]init];
    car.run(10);
    NSString *str  =  car.drive(20);
    NSLog(@"%@", str);複製程式碼
輸出如下:
2016-09-03 12:29:31.221 01-Block開發中使用場景[14981:995644] car run 10 meter
2016-09-03 12:29:31.222 01-Block開發中使用場景[14981:995644] I drive 20 meters in the car.複製程式碼

其實上面的run和drive函式我們完全可以用方法來實現相同的功能,但是那樣我們只能使用[object methodName]的方式呼叫,沒法使用點語法實現鏈式呼叫。

其實上面的點語法呼叫函式,就是呼叫該函式的的getter方法。

這裡我們先了解可以使用點語法來實現和方法相同的功能,下面我們會講到鏈式呼叫,就是使用此處的知識點。


block儲存程式碼塊

這個應該也是我們平時開發中用的比較多的,比如代替delegate實現回撥。 具體可以看這篇文章: block實現回撥

下面我們來看看block是如何實現回撥的,首先搞清楚回撥的概念就是,下面是通俗的解釋:

你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。在這個例子裡,你的電話號碼就叫回撥函式,你把電話留給店員就叫登記回撥函式,店裡後來有貨了叫做觸發了回撥關聯的事件,店員給你打電話叫做呼叫回撥函式,你到店裡去取貨叫做響應回撥事件

上面的文章提到一個使用場景:

在tableview的cell上有一個按鈕,由於採用MVC架構,cell的類和tableviewController類檔案是分離的,我們想實現點選cell上面的按鈕的時候可以回撥tableviewController的內部方法來實現某些功能。

1、定義回撥函式

首先我們在cell內定義一個回撥函式,這裡採用block來實現,具體實現為cell的一個property。

//block名字為callBack ,傳入引數NSString
@property(copy, nonatomic) void (^callBack)(NSString *);複製程式碼
2、呼叫回撥函式

接下來在tableviewController裡面呼叫回撥函式,也就是給cell的block屬性賦值


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    cell.callBack = ^(SGAttentionModel *model) {
        //do something
    };
    return cell;
}複製程式碼
3、觸發回撥關聯事件

然後當按鈕點選的時候,就觸發回撥關聯事件

    [self.button addTarget:self action:@selector(addFollow) forControlEvents:UIControlEventTouchUpInside];複製程式碼
4、響應回撥事件

觸發回撥關聯事件之後,cell就響應回撥事件

- (void)addFollow
{
    if (self.callBack) {
        self.callBack(self.nsstring));
    }
}複製程式碼

當然上述實現完全可以用delegate來實現,但是使用block更加簡潔

上面的場景在步驟2給cell的block屬性賦值一個程式碼塊,然後在步驟4,cell呼叫該程式碼塊實現功能。可以看到block可以實現儲存、傳遞程式碼塊,然後在合適的時候呼叫的功能。

這裡是跨類傳遞block給另外一個類,當然你也可以在類裡面的一個地方儲存一個block,然後在類的另外一個地方呼叫。


block實現鏈式程式設計

說完了上面的基礎知識,我們下面就需要使用這些基礎知識來實現鏈式程式設計和函數語言程式設計。

場景:

實現加法計算,比如我需要計算1+2+5+14。通常做法如下:

定義加法函式:

-(NSInteger)addWithParam1:(NSInteger)param1 param2:(NSInteger)param2 {
    return param1 + param2;
}複製程式碼

然後呼叫:

    NSInteger result = [self addWithParam1:1 param2:2];
    result = [self addWithParam1:result param2:5];
    result = [self addWithParam1:result param2:14];
    NSLog(@"%zd",result);複製程式碼

有多少個數字需要相加,我們就需要呼叫多少次這個方法,相當麻煩。

我們想實現如下效果的呼叫,類似於masonry,也就是所謂的鏈式程式設計,看起來就十分優雅。

int reslut = [NSObject makeCalculate:^(CalculateManager *mgr) {
        mgr.add(5).add(6).add(7).add(10);
    }];複製程式碼

下面我們就來看看具體的實現過程吧。

1、先定義一個NSObject的分類如下:

#import 

#import "CalculateManager.h"

@interface NSObject (Calculate)

+ (int)makeCalculate:(void(^)(CalculateManager *))block;
@end


==============================================================================

#import "NSObject+Calculate.h"
#import "CalculateManager.h"

@implementation NSObject (Calculate)
+ (int)makeCalculate:(void (^)(CalculateManager *))block
{
    // 建立計算管理者
    CalculateManager *mgr = [[CalculateManager alloc] init];

    // 執行計算
    block(mgr);

    return mgr.result;
}
@end複製程式碼
2、繼續定義一個類實現計算過程,比如add:
#import 

@interface CalculateManager : NSObject

@property (nonatomic, assign) int result;

- (CalculateManager *(^)(int))add;
@end


=======================================================


#import "CalculateManager.h"

@implementation CalculateManager

- (CalculateManager * (^)(int))add
{
    return ^(int value){
        _result += value;
        return self;
    };
}

@end複製程式碼
3、然後呼叫:
   int reslut = [NSObject makeCalculate:^(CalculateManager *mgr) {
        mgr.add(5).add(6).add(7).add(10);

    }];

    NSLog(@"%zd",reslut);複製程式碼

要實現鏈式呼叫的一個關鍵點:就是每次呼叫add方法必須返回自身,然後才可以繼續呼叫,如此一致迴圈下去,實現這一切都是block的功勞。

4、實現過程分析:
  1. 上面的步驟3,呼叫nsobject的分類方法makeCalculate:^(CalculateManager *mgr)block,該方法的引數是一個block,我們在這裡傳遞一個定義好的block到該函式。block的實現是mgr.add(5).add(6).add(7).add(10)
  2. 回到步驟1,是分類方法makeCalculate:^(CalculateManager *mgr)block的具體實現,該方法內部初始化一個CalculateManager例項物件mgr,然後作為block的引數傳入block,也就是步驟3的block內部的mgr引數,然後呼叫該block,也就是上一步實現的這句程式碼mgr.add(5).add(6).add(7).add(10),然後返回執行完畢後的結果,也就是mgr.result。
  3. 回到步驟2,是鏈式呼叫程式碼mgr.add(5).add(6).add(7).add(10)的關鍵,可以看到add方法返回的是一個block,該block的實現是累加傳遞進來的值然後賦值給屬性result儲存下來,然後返回值是self,也就是CalculateManager例項物件。這樣又可以實現點語法繼續呼叫add方法。

block實現函數語言程式設計

不瞭解什麼是函式程式設計的童鞋可以看看這篇文章,作為一個入門瞭解:

函數語言程式設計初探

函式程式設計有兩個好處:

  1. 去掉了中間變數
  2. 把運算過程寫成一系列的函式巢狀呼叫,邏輯更加清楚

還是上面的例子,不過這次我們想如下寫:

    CalculateManager *mgr = [[CalculateManager alloc] init];
    [[[[mgr calculate:^(int result){
       // 存放所有的計算程式碼
        result += 5;
        result *= 5;
        return result;
    }]printResult:^(int result) {
        NSLog(@"第一次計算結果為:%d",result);
    }]calculate:^int(int result) {
        result -= 2;
        result /= 3;
        return result;
    }]printResult:^(int result) {
        NSLog(@"第二次計算結果為:%d",result);
    }];複製程式碼

可以看到計算函式calculate和輸出函式printResult可以一直迴圈巢狀呼叫,所有的運算過程全部聚在一起,看起來邏輯更加清楚。

下面來看看如何實現:

#import 
@interface CalculateManager : NSObject
@property (nonatomic, assign) int result;
- (instancetype)calculate:(int(^)(int))calculateBlock;
-(instancetype)printResult:(void(^)(int))printBlock;
@end


===========================================================

#import "CalculateManager.h"

@implementation CalculateManager
- (instancetype)calculate:(int (^)(int))calculateBlock
{
    _result =  calculateBlock(_result);
    return self;
}

-(instancetype)printResult:(void(^)(int))printBlock{
    printBlock(_result);
    return self;
}
@end

`複製程式碼

上面兩個函式的關鍵點在於每次都必須返回self,這樣才可以繼續巢狀呼叫其他函式。函式的內部實現是做一些內部處理,然後傳入引數來呼叫block。


總結

剛開始理解block可能有些費盡,覺得非常彆扭。但是如果你把block當初普通的OC物件來理解,就可以馬上理解上面列出的的block使用場景了。唯一的不同是block是函式,可以實現函式的所有功能。

這讓他即可以像物件一樣被傳遞、儲存、當做引數,也可以像函式一樣實現功能。如果還是不太理解(我覺得block怪異的語法是理解的一大障礙),那麼可以先去看看python的Lambda,同樣是匿名函式,但是更好理解。

更多文章請訪問我的個人部落格:blog.ximu.site

相關文章