iOS開發系列--Objective-C之協議、程式碼塊、分類

KenshinCui發表於2014-07-26

概述

ObjC的語法主要基於smalltalk進行設計的,除了提供常規的物件導向特性外,還增加了很多其他特性,這一節將重點介紹ObjC中一些常用的語法特性。當然這些內容雖然和其他高階語言命名不一樣,但是我們都可以在其中找到他們的影子,在文章中我也會對比其他語言進行介紹,這一節的重點內容如下:

  1. 協議protocol
  2. 程式碼塊block
  3. 分類category

協議protocol

在ObjC中使用@protocol定義一組方法規範,實現此協議的類必須實現對應的方法。熟悉物件導向的童鞋都知道介面本身是物件行為描述的協議規範。也就是說在ObjC中@protocol和其他語言的介面定義是類似的,只是在ObjC中interface關鍵字已經用於定義類了,因此它不會再像C#、Java中使用interface定義介面了。

假設我們定義了一個動物的協議AnimalDelegate,人員Person這個類需要實現這個協議,請看下面的程式碼:

AnimalDelegate.h

//
//  AnimalDelegate.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//


//定義一個協議
@protocol AnimalDelegate <NSObject>

@required //必須實現的方法
-(void)eat;

@optional //可選實現的方法
-(void)run;
-(void)say;
-(void)sleep;

@end

 

Person.h

//
//  Person.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "AnimalDelegate.h"

@interface Person : NSObject<AnimalDelegate>

-(void)eat;

@end

Person.m

//
//  Person.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "Person.h"

@implementation Person

-(void)eat{
    NSLog(@"eating...");
}

@end

這裡需要說明幾點:

  1. 一個協議可以擴充套件自另一個協議,例如上面AnimalDelegate就擴充套件自NSObject,如果需要擴充套件多個協議中間使用逗號分隔;
  2. 和其他高階語言中介面不同的是協議中定義的方法不一定是必須實現的,我們可以通過關鍵字進行@required和@optional進行設定,如果不設定則預設是@required(注意ObjC是弱語法,即使不實現必選方法編譯執行也不會報錯);
  3. 協議通過<>進行實現,一個類可以同時實現多個協議,中間通過逗號分隔;
  4. 協議的實現只能在類的宣告上,不能放到類的實現上(也就是說必須寫成@interface Person:NSObject<AnimalDelegate>而不能寫成@implementation Person<AnimalDelegate>);
  5. 協議中不能定義屬性、成員變數等,只能定義方法;

事實上在ObjC中協議的更多作用是用於約束一個類必須實現某些方法,而從物件導向的角度而言這個類跟介面並不一定存在某種自然關係,可能是兩個完全不同意義上的事物,這種模式我們稱之為代理模式(Delegation)。在Cocoa框架中大量採用這種模式實現資料和UI的分離,而且基本上所有的協議都是以Delegate結尾。

現在假設需要設計一個按鈕,我們知道按鈕都是需要點選的,在其他語言中通常會引入事件機制,只要使用者訂閱了點選事件,那麼點選的時候就會觸發執行這個事件(這是物件之間解耦的一種方式:程式碼注入)。但是在ObjC中沒有事件的定義,而是使用代理來處理這個問題。首先在按鈕中定義按鈕的代理,同時使用協議約束這個代理(事件的觸發者)必須實現協議中的某些方法,當按鈕處理過程中檢視代理是否實現了這個方法,如果實現了則呼叫這個方法。

KCButton.h

//
//  KCButton.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
@class KCButton;

//一個協議可以擴充套件另一個協議,例如KCButtonDelegate擴充套件了NSObject協議
@protocol KCButtonDelegate <NSObject>

@required //@required修飾的方法必須實現
-(void)onClick:(KCButton *)button;

@optional //@optional修飾的方法是可選實現的
-(void)onMouseover:(KCButton *)button;
-(void)onMouseout:(KCButton *)button;

@end

@interface KCButton : NSObject

#pragma mark - 屬性
#pragma mark 代理屬性,同時約定作為代理的物件必須實現KCButtonDelegate協議
@property (nonatomic,retain) id<KCButtonDelegate> delegate;

#pragma mark - 公共方法
#pragma mark 點選方法
-(void)click;

@end

KCButton.m

//
//  KCButton.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCButton.h"

@implementation KCButton

-(void)click{
    NSLog(@"Invoke KCButton's click method.");
    //判斷_delegate例項是否實現了onClick:方法(注意方法名是"onClick:",後面有個:)
    //避免未實現ButtonDelegate的類也作為KCButton的監聽
    if([_delegate respondsToSelector:@selector(onClick:)]){
        [_delegate onClick:self];
    }
}

@end

MyListener.h

//
//  MyListener.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
@class KCButton;
@protocol KCButtonDelegate;

@interface MyListener : NSObject<KCButtonDelegate>
-(void)onClick:(KCButton *)button;
@end

MyListener.m

//
//  MyListener.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "MyListener.h"
#import "KCButton.h"

@implementation MyListener
-(void)onClick:(KCButton *)button{
    NSLog(@"Invoke MyListener's onClick method.The button is:%@.",button);
}
@end

main.m

//
//  main.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "KCButton.h"
#import "MyListener.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        KCButton *button=[[KCButton alloc]init];
        MyListener *listener=[[MyListener alloc]init];
        button.delegate=listener;
        [button click];
        /* 結果:
         Invoke KCButton's click method.
         Invoke MyListener's onClick method.The button is:<KCButton: 0x1001034c0>.
         */
    }
    return 0;
}

我們通過例子模擬了一個按鈕的點選過程,有點類似於Java中事件的實現機制。通過這個例子我們需要注意以下幾點內容:

  1. id可以表示任何一個ObjC物件型別,型別後面的”<協議名>“用於約束作為這個屬性的物件必須實現該協議(注意:使用id定義的物件型別不需要加“*”);
  2. MyListener作為事件觸發者,它實現了KCButtonDelegate代理(在ObjC中沒有名稱空間和包的概念,通常通過字首進行類的劃分,“KC”是我們自定義的字首)
  3. 在.h檔案中如果使用了另一個檔案的類或協議我們可以通過@class或者@protocol進行宣告,而不必匯入這個檔案,這樣可以提高編譯效率(注意有些情況必須使用@class或@protocol,例如上面KCButton.h中上面宣告的KCButtonDelegate協議中用到了KCButton類,而此檔案下方的KCButton類宣告中又使用了KCButtonDelegate,從而形成在一個檔案中互相引用關係,此時必須使用@class或者@protocol宣告,否則編譯階段會報錯),但是在.m檔案中則必須匯入對應的類宣告檔案或協議檔案(如果不匯入雖然語法檢查可以通過但是編譯連結會報錯);
  4. 使用respondsToSelector方法可以判斷一個物件是否實現了某個方法(需要注意方法名不是”onClick”而是“onClick:”,冒號也是方法名的一部分);

屬性中的(nonatomic,retain)不是這篇文章的重點,在接下來的文章中我們會具體介紹。

程式碼塊Block

在C#非同步程式設計時我們經常進行函式回撥,由於函式呼叫是非同步執行的,我們如果想讓一個操作執行完之後執行另一個函式,則無法按照正常程式碼書寫順序進行程式設計,因為我們無法獲知前一個方法什麼時候執行結束,此時我們經常會用到匿名委託或者lambda表示式將一個操作作為一個引數進行傳遞。其實在ObjC中也有類似的方法,稱之為程式碼塊(Block)。Block就是一個函式體(匿名函式),它是ObjC對於閉包的實現,在塊狀中我們可以持有或引用區域性變數(不禁想到了lambda表示式),同時利用Block你可以將一個操作作為一個引數進行傳遞(是不是想起了C語言中的函式指標)。在下面的例子中我們將使用Block實現上面的點選監聽操作:

KCButton.h

//
//  KCButton.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
@class KCButton;
typedef void(^KCButtonClick)(KCButton *);

@interface KCButton : NSObject

#pragma mark - 屬性
#pragma mark 點選操作屬性
@property (nonatomic,copy) KCButtonClick onClick;
//上面的屬性定義等價於下面的程式碼
//@property (nonatomic,copy) void(^ onClick)(KCButton *);

#pragma mark - 公共方法
#pragma mark 點選方法
-(void)click;
@end

KCButton.m

//
//  KCButton.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCButton.h"


@implementation KCButton

-(void)click{
    NSLog(@"Invoke KCButton's click method.");
    if (_onClick) {
        _onClick(self);
    }
}

@end

main.m

//
//  main.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "KCButton.h"


int main(int argc, const char * argv[]) {

    KCButton *button=[[KCButton alloc]init];
    button.onClick=^(KCButton *btn){
        NSLog(@"Invoke onClick method.The button is:%@.",btn);
    };
    [button click];
    /*結果:
     Invoke KCButton's click method.
     Invoke onClick method.The button is:<KCButton: 0x1006011f0>.
     */
    
    
    return 0;
}

上面程式碼中使用Block同樣實現了按鈕的點選事件,關於Block總結如下:

  1. Block型別定義:返回值型別(^ 變數名)(引數列表)注意Block也是一種型別);
  2. Block的typedef定義:返回值型別(^型別名稱)(引數列表)
  3. Block的實現:^(引數列表){操作主體}
  4. Block中可以讀取塊外面定義的變數但是不能修改,如果要修改那麼這個變數必須宣告_block修飾;

分類Category

當我們不改變原有程式碼為一個類擴充套件其他功能時我們可以考慮繼承這個類進行實現,但是這樣一來使用時就必須定義成新實現的子類才能擁有擴充套件的新功能。如何在不改變原有類的情況下擴充套件新功能又可以在使用時不必定義新型別呢?我們知道如果在C#中可以使用擴充套件方法,其實在ObjC中也有類似的實現,就是分類Category。利用分類,我們就可以在ObjC中動態的為已有類新增新的行為(特別是系統或框架中的類)。在C#中字串有一個Trim()方法用於去掉字串前後的空格,使用起來特別方便,但是在ObjC中卻沒有這個方法,這裡我們不妨通過Category給NSString新增一個stringByTrim()方法:

NSString+Extend.h

//
//  NSString+Extend.h
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSString (Extend)
-(NSString *)stringByTrim;
@end

NSString+Extend.m

//
//  NSString+Extend.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "NSString+Extend.h"

@implementation NSString (Extend)
-(NSString *)stringByTrim{
    NSCharacterSet *character= [NSCharacterSet whitespaceCharacterSet];
    return [self stringByTrimmingCharactersInSet:character];
}
@end

main.m

//
//  main.m
//  Protocol&Block&Category
//
//  Created by Kenshin Cui on 14-2-2.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "NSString+Extend.h"


int main(int argc, const char * argv[]) {

    NSString *name=@" Kenshin Cui ";
    name=[name stringByTrim];
    NSLog(@"I'm %@!",name); //結果:I'm Kenshin Cui!
    
    return 0;
}

通過上面的輸出結果我們可以看出已經成功將@” Kenshin Cui ”兩端的空格去掉了。分類檔名一般是“原有類名+分類名稱”,分類的定義是通過在原有類名後加上”(分類名)”來定義的(注意宣告檔案.h和實現檔案.m都是如此)。

相關文章