Categories and Extensions in Objective-C

前言

類別是Objective-C語言的一種特性,容許程序員向現有類添加新方法,就像C#的extension同樣。 可是,不要將C#中的extension和Objective-C的extension混淆。 Objective-C的extension是categories的特例,extension必須定義在.m文件中。程序員

extension和categories功能強大,具備許多潛在用途。 主要有如下三種:objective-c

  1. 首先,categories能夠將類的接口和實現分紅幾個文件,這爲大型項目提供了模塊化的可能。
  2. 其次,categories容許程序員修復現有類(例如,NSString)中的錯誤,而無需對其進行子類化。
  3. 第三,實現了相似於C#和其餘Simula類語言中的protected和private方法。

Categories

categories是同一個類的一組相關方法,categories中定義的全部方法均可以經過類得到,就好像它們是在.h文件中定義的同樣。 舉個例子,參考Person類。 若是這是一個大型項目,Person可能有許多方法,從基本行爲到與其餘人的交互到身份檢查。 API可能要求經過單個類提供全部這些方法,但若是每一個組都存儲在單獨的文件中,則開發人員能夠更輕鬆地進行維護。 此外,categories消除了每次更改單個方法時從新編譯整個類的須要,這能夠節省大型項目的時間。編程

咱們來看看如何使用categories來實現這一目標。 咱們從一個普通的類接口和相應的實現開始:編程語言

// Person.h
@interface Person : NSObject
 
@property (readonly) NSMutableArray* friends;
@property (copy) NSString* name;
 
- (void)sayHello;
- (void)sayGoodbye;
 
@end
 
 
// Person.m
#import "Person.h"
 
@implementation Person
 
@synthesize name = _name;
@synthesize friends = _friends;
 
-(id)init{
    self = [super init];
    if(self){
        _friends = [[NSMutableArray alloc] init];
    }
 
    return self;
}
 
- (void)sayHello {
    NSLog(@"Hello, says %@.", _name);
}
 
- (void)sayGoodbye {
    NSLog(@"Goodbye, says %@.", _name);
}
@end
複製代碼

這裏沒什麼新東西 - 只有一個具備兩個屬性的Person類(咱們的categories將使用friends屬性)和兩個方法。 接下來,咱們將使用一個categories來存儲一些與其餘Person實例交互的方法。 建立一個新文件,但不使用類,而是使用Objective-C Category模板。ide

Figure 28 Creating the PersonRelations class

正如所料,這將建立兩個文件:用於保存接口的頭文件和實現文件。 可是,這些看起來與咱們一直在使用的略有不一樣。 首先,咱們來看看界面:模塊化

// Person+Relations.h
#import <Foundation/Foundation.h>
#import "Person.h"
 
@interface Person (Relations)
 
- (void)addFriend:(Person *)aFriend;
- (void)removeFriend:(Person *)aFriend;
- (void)sayHelloToFriends;
 
@end
複製代碼

咱們在擴展的類名後面的括號中包含了categories名稱,而不是正常的@interface聲明。 categories名稱能夠是任何名稱,只要它不與同一個類的其餘categories衝突便可。 categories的文件名應該是類名,後跟加號,後跟categories的名稱(例如,Person + Relations.h)。工具

因此,這定義了咱們categories的接口。 咱們在這裏添加的任何方法都將在運行時添加到原始的Person類中。 例如addFriendremoveFriendsayHelloToFriends方法都在Person.h中定義,但咱們能夠保持咱們的功能封裝和可維護。 另請注意,您必須導入原始類Person.h的標頭。 categories實現遵循相似的模式:spa

// Person+Relations.m
#import "Person+Relations.h"
 
@implementation Person (Relations)
 
- (void)addFriend:(Person *)aFriend {
    [[self friends] addObject:aFriend];
}
 
- (void)removeFriend:(Person *)aFriend {
    [[self friends] removeObject:aFriend];
}
 
- (void)sayHelloToFriends {
    for (Person *friend in [self friends]) {
        NSLog(@"Hello there, %@!", [friend name]);
    }
}
 
@end
複製代碼

上述代碼實現了Person + Relations.h中的全部方法。 就像categories的接口文件同樣,categories名稱出如今類名後面的括號中。 實現中的categories名稱應與接口文件中的categories名稱匹配。設計

另請注意,沒法在categories中定義其餘屬性或實例變量。 categories必須使用存儲在主類中的數據(在此實例中爲Friend)。3d

也能夠經過簡單地從新定義Person + Relations.m中的方法來覆蓋Person.m中包含的實現。 這能夠用來修補現有的類; 可是,若是您有問題的替代解決方案,則不建議使用,由於沒法覆蓋categories中定義的實現。 也就是說,與類層次結構不一樣,categories是一個扁平的組織結構 - 若是在兩個單獨的categories中實現相同的方法,則運行時沒法肯定使用哪一個categories。

要使用categories,您必須進行的惟一更改是導入categories的頭文件。 正如您在下面的示例中所看到的,Person類能夠訪問Person.h中定義的方法以及Person + Relations.h類別中定義的方法:

// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Person+Relations.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *joe = [[Person alloc] init];
        joe.name = @"Joe";
        Person *bill = [[Person alloc] init];
        bill.name = @"Bill";
        Person *mary = [[Person alloc] init];
        mary.name = @"Mary";
 
        [joe sayHello];
        [joe addFriend:bill];
        [joe addFriend:mary];
        [joe sayHelloToFriends];
    }
    return 0;
}
複製代碼

這就是在Objective-C中建立categories的所有內容。

Protected Methods

重申一下,全部Objective-C方法都是public的,沒有語法能夠將它們標記爲private或protected。Objective-C程序能夠將categories與.h/.m的範式結合起來,而不是使用所謂真正的protected方法,以實現相同的結果。

這個想法很簡單:將「protected」方法聲明爲單獨頭文件中的categories。 這使得子類可以「選擇加入」protected的方法,而不相關的類像往常同樣使用「public」頭文件。 例如,採用標準的Ship接口:

// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
- (void)shoot;
 
@end
複製代碼

正如咱們屢次看到的那樣,這定義了一種名爲shoot的公共方法。 要聲明受保護的方法,咱們須要在專用頭文件中建立Ship categories:

// Ship_Protected.h
#import <Foundation/Foundation.h>
 
@interface Ship(Protected)
 
- (void)prepareToShoot;
 
@end
複製代碼

任何須要訪問受保護方法的類(即父類和任何子類)均可以簡單地導入Ship_Protected.h。 例如,Ship實現應該爲受保護的方法定義默認實現:

// Ship.m
#import "Ship.h"
#import "Ship_Protected.h"
 
@implementation Ship {
    BOOL _gunIsReady;
}
 
- (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@"Firing!");
}
 
- (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@"Preparing the main weapon...");
}
@end
複製代碼

請注意,若是咱們沒有導入Ship_Protected.h,則此prepareToShoot實現將是一個私有方法。 若是沒有導入Ship_Protected.h,子類將沒法訪問此方法。 讓咱們將Ship子類化,看看它是如何工做的。 咱們稱之爲ResearchShip:

// ResearchShip.h
#import "Ship.h"
 
@interface ResearchShip : Ship
 
- (void)extendTelescope;
 
@end
複製代碼

這是一個普通的子類接口 - 它不該該導入Ship_Protected.h,由於這會使受保護的方法對任何導入ResearchShip.h的人均可用,這正是咱們試圖避免的。 最後,子類的實現導入受保護的方法,並(可選)覆蓋它們:

// ResearchShip.m
#import "ResearchShip.h"
#import "Ship_Protected.h"
 
@implementation ResearchShip
 
- (void)extendTelescope {
    NSLog(@"Extending the telescope");
}
 
// Override protected method
- (void)prepareToShoot {
    NSLog(@"Oh shoot! We need to find some weapons!");
}
 
@end
複製代碼

要在Ship_Protected.h中強制執行方法的受保護狀態,不容許其餘類導入它。 他們只會導入超類和子類的普通「公共」接口:

// main.m
#import <Foundation/Foundation.h>
#import "Ship.h"
#import "ResearchShip.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Ship *genericShip = [[Ship alloc] init];
        [genericShip shoot];
 
        Ship *discoveryOne = [[ResearchShip alloc] init];
        [discoveryOne shoot];
 
    }
    return 0;
}
複製代碼

因爲main.m,Ship.h和ResearchShip.h都沒有導入受保護的方法,所以該代碼沒法訪問它們。 嘗試添加[discoveryOne prepareToShoot]方法 - 它會拋出編譯器錯誤,由於找不到prepareToShoot聲明。

總而言之,能夠經過將受保護的方法放在專用的頭文件中並將該頭文件導入須要訪問受保護方法的實現文件來模擬受保護的方法。 沒有其餘文件應該導入受保護的header。

雖然此處介紹的工做流程是一個徹底有效的組織工具,但請記住,Objective-C從未打算支持受保護的方法。 能夠將其視爲構建Objective-C方法的替代方法,而不是直接替換C# / Simula樣式的受保護方法。 尋找構建類的另外一種方法一般更好,而不是強迫Objective-C代碼像C#程序同樣運行。

說明

category的一個最大問題是您沒法可靠地覆蓋同一類的categories中定義的方法。 例如,若是您在Person(Relations)中定義了一個addFriend:方法,後來決定經過Person(Security)類別更改addFriend: 實現,那麼運行時沒法知道它應該使用哪一種方法,由於categories 根據定義是一個扁平的組織結構。 對於這些狀況,您須要恢復到傳統的子類化範例。

此外,重要的是要注意categories不能添加實例變量。 這意味着您沒法在categories中聲明新屬性,由於它們只能在主實現中合成。 此外,儘管categories在技術上確實能夠訪問其類的實例變量,但最好經過其公共接口訪問它們,以保護categories免受主實現文件中的潛在更改。

Extensions

Extensions(也稱爲class extensions)是一種特殊類型的類,它要求在關聯類的主實現塊中定義它們的方法,而不是在category中定義的實現。 這能夠用於覆蓋公開聲明的屬性屬性。 例如,有時能夠方便地將只讀屬性更改成類實現中的讀寫屬性。 考慮Ship類的普通接口:

// Ship.h
#import <Foundation/Foundation.h>
#import "Person.h"
 
@interface Ship : NSObject
 
@property (strong, readonly) Person *captain;
 
- (id)initWithCaptain:(Person *)captain;
 
@end
複製代碼

類擴展能夠覆蓋class中的@property定義。 這使您有機會在實現文件中將該屬性從新聲明爲readwrite。 從語法上講,擴展看起來像一個空的category聲明:

// Ship.m
#import "Ship.h"
 
 
// The class extension.
@interface Ship()
 
@property (strong, readwrite) Person *captain;
 
@end
 
 
// The standard implementation.
@implementation Ship
 
@synthesize captain = _captain;
 
- (id)initWithCaptain:(Person *)captain {
    self = [super init];
    if (self) {
        // This WILL work because of the extension.
        [self setCaptain:captain];
    }
    return self;
}
 
@end
複製代碼

注意@interface指令後附加到類名的()。 這是將其標記爲擴展而不是普通接口或category的緣由。 必須在類的主實現塊中聲明擴展中出現的任何屬性或方法。 在這種狀況下,咱們不會添加任何新字段 - 咱們會覆蓋現有字段。 可是與category不一樣,擴展能夠向類中添加額外的實例變量,這就是爲何咱們可以在類擴展中聲明屬性而不是category。

由於咱們使用readwrite屬性從新聲明瞭captain屬性,因此initWithCaptain:方法能夠在自身上使用setCaptain:accessor。 若是要刪除擴展名,屬性將返回其只讀狀態,編譯器會報錯。 使用Ship類的客戶端不該該導入實現文件,所以captain屬性將保持只讀。

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Ship.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Person *heywood = [[Person alloc] init];
        heywood.name = @"Heywood";
        Ship *discoveryOne = [[Ship alloc] initWithCaptain:heywood];
        NSLog(@"%@", [discoveryOne captain].name);
 
        Person *dave = [[Person alloc] init];
        dave.name = @"Dave";
        // This will NOT work because the property is still read-only.
        [discoveryOne setCaptain:dave];
 
    }
    return 0;
}
複製代碼

Private Methods

擴展的另外一個常見用例是聲明私有方法。 在上一章中,咱們看到了如何經過在實現文件中的任何位置添加私有方法來聲明私有方法。 可是,在Xcode 4.3以前,狀況並不是如此。 建立私有方法的規範方法是使用類擴展來向前聲明它。 讓咱們經過稍微改變前一個示例中的Ship的頭文件來看一下這個:

// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
- (void)shoot;
 
@end
複製代碼

接下來,咱們將從新建立討論私有方法時使用的示例。 咱們須要在類擴展中向前聲明它,而不是簡單地將私有prepareToShoot方法添加到實現中。

// Ship.m
#import "Ship.h"
 
// The class extension.
@interface Ship()
 
- (void)prepareToShoot;
 
@end
 
// The rest of the implementation.
@implementation Ship {
    BOOL _gunIsReady;
}
 
- (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@"Firing!");
}
 
- (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@"Preparing the main weapon...");
}
 
@end
複製代碼

編譯器確保擴展方法在主實現塊中實現,這就是它做爲forward-declaration的緣由。 然而,由於擴展被封裝在實現文件中,因此其餘對象不該該知道它,爲咱們提供了另外一種模擬私有方法的方法。 雖然較新的編譯器能夠爲您節省這些麻煩,但瞭解類擴展的工做原理仍然很重要,由於它是直到最近纔開始利用Objective-C程序中的私有方法的經常使用方法。

總結

本章介紹了Objective-C編程語言中兩個更獨特的概念:category和extension。 category是擴展示有類的API的一種方式,extension是一種在主接口文件的API以外添加所需方法的方法。 這兩個最初都是爲了減輕維護大型代碼庫的負擔而設計的。

相關文章
相關標籤/搜索