每一個(gè)對象都應(yīng)該有明確的任務(wù),如為具體信息建模,顯示可視化內(nèi)容或者控制信息流。 另外正如你所知道的,一個(gè)類的接口定義了其利用一個(gè)對象來幫助它完成任務(wù)的連接方式。
有時(shí)你會發(fā)現(xiàn)你希望通過添加某些方法來擴(kuò)展現(xiàn)有的類,但這只在某些情況下是有用的。 舉個(gè)例子,你發(fā)現(xiàn)你的應(yīng)用程序經(jīng)常需要在一個(gè)可視化界面顯示一些字符串信息。 那么除了在每次你需要顯示字符串時(shí)創(chuàng)造一個(gè)字符串繪圖對象來使用外,給 NSString 類自己本身賦予可以在屏幕上繪制字符的能力會更有意義。
在這種情況下,對原始的、主要的類的接口增加功能方法并不總是可行的。 因?yàn)樵诖蠖鄶?shù)應(yīng)用字符串對象的程序中,繪圖能力并不總是被要求的。 例如,在 NSString 類中,你不能修改原來的接口或是繼承,因?yàn)樗且粋€(gè)框架類。
此外,上述方法對現(xiàn)有類的子類也是沒有意義的,因?yàn)槟憧赡芟M愕睦L圖能力不僅對原始的 NSString 類有效,也有對該類子類有效,如 NSMutableString 類。 另外雖然 NSString 類在 OS X 和 iOS 兩個(gè)操作系統(tǒng)內(nèi)均可使用,但相關(guān)繪圖能力的代碼在每個(gè)操作系統(tǒng)內(nèi)是不同的,所以你需要在每個(gè)操作系統(tǒng)內(nèi)使用不同的子類。
然而,Objective-C 允許你通過 categories 和類擴(kuò)展來對已有的類中添加你自定義的方法。
如果你需要添加方法到現(xiàn)有類,是為了添加某些功能使你自己的應(yīng)用程序在完成某些人任務(wù)時(shí)更容易,那么使用 category 是最方便的方法。
使用 @ interface 關(guān)鍵字來聲明一個(gè) category ,就像標(biāo)準(zhǔn)的 Objective-C 類的描述一樣,但并不表示這個(gè) category 從任何一個(gè)子類繼承。 另外它指定 category 的名稱在括號內(nèi),像這樣:
@interface ClassName (CategoryName)
@end
一個(gè) category 可以聲明任何類,即使是在沒有原始代碼的類(如標(biāo)準(zhǔn)的 Cocoa 或 Cocoa Touch 的類)。 你在 category 中聲明的任何方法都可以被原始類和任何原始類的子類所實(shí)例化。 同時(shí)在運(yùn)行時(shí),你在 category 里添加的方法和由原始類實(shí)現(xiàn)的方法之間是沒有區(qū)別的。
請考慮在之前章節(jié)提過的 XYZPerson 類,它具有一個(gè)人的姓氏和名字的屬性。 如果你在寫一個(gè)記錄的應(yīng)用程序,你會發(fā)現(xiàn)你經(jīng)常需要顯示一個(gè)按姓氏排列的名單,像這樣:
Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
如果你不想在每次顯示這個(gè)列表的時(shí)候再編寫代碼來生成適當(dāng)?shù)?lastName , firstName 字符串, 那么你可以向 XYZPerson 類如下所示添加一個(gè) category :
#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end
在此示例中,名為 XYZPersonNameDisplayAdditions 的 category 聲明了一個(gè)額外的方法以返回必需的字符串。
一個(gè) category 通常是在單獨(dú)的頭文件中聲明的,并在單獨(dú)的源代碼文件被實(shí)現(xiàn)。 例如在 XYZPerson 類中,你可能會聲明一個(gè) category 在 XYZPerson+XYZPersonNameDisplayAdditions.h 的頭文件中。
雖然用 category 添加的任何方法都可用于此類及其子類的所有實(shí)例中,但你仍需要在任何要使用添加的方法的源代碼文件中導(dǎo)入含有 category 的頭文件,否則你可能會遇到編譯器警告和錯(cuò)誤。
Category 的實(shí)現(xiàn)如下所示:
#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end
一旦你已經(jīng)聲明一個(gè) category 并繼承這些方法,你可以在此類的任何實(shí)例中使用這些方法,就好像他們是原始類接口的一部分一樣:
#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
lastName:@"Doe"];
XYZShoutingPerson *shoutingPerson =
[[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
lastName:@"Robinson"];
NSLog(@"The two people are %@ and %@",
[person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end
除了可以向現(xiàn)有的類添加方法,你還可以使用 categories 把多功能的源代碼文件中一個(gè)復(fù)雜的類拆分。 例如,你把一個(gè)自定義用戶界面元素的繪圖代碼放在一個(gè)單獨(dú)的 category 中,來執(zhí)行一些其余的功能如幾何計(jì)算、 顏色和漸變等,這就是一個(gè)特別復(fù)雜的類的例子。 另外你可以給類別的方法提供不同的實(shí)現(xiàn),具體取決于你正在寫一個(gè) OS X 還是 iOS 的應(yīng)用程序。
Categories 可用于聲明類方法或成員方法,但并非通常適合聲明附加屬性。 在一個(gè) category 的接口中包含屬性聲明時(shí)編譯器不會報(bào)錯(cuò),但是不能在一個(gè) category 中聲明一個(gè)附加的成員變量。 這意味著,編譯器不會為該屬性合成任何成員變量,也不合成任何屬性訪問方法。 在類的實(shí)現(xiàn)過程中,你可以編寫你自己的訪問方法,但是你不能來跟蹤該屬性的值,除非原始類中已有了該成員變量。
添加一個(gè)傳統(tǒng)屬性的唯一方式——也就是從現(xiàn)有類支持一個(gè)新的成員變量——是使用類擴(kuò)展,如 Class Extensions Extend the Internal Implementation。
注: Cocoa 和 Cocoa Touch 包括了大量的主要框架類的 categories。
這一章的導(dǎo)言中提到的字符串繪圖功能事實(shí)上已經(jīng)由 OS X 中名為 NSStringDrawing 的 category 提供給 NSString 類了,其中包括 drawAtPoint:withAttributes: 和 drawInRect:withAttributes: 方法。 對于 iOS ,UIStringDrawing category 包括 drawAtPoint: withFont 方法和 drawInRect: withFont 方法。
因?yàn)樵谝粋€(gè) category 中聲明的方法已經(jīng)添加到現(xiàn)有的類中,所以你需要非常小心有關(guān)方法名的定義問題。
如果在一個(gè) category 中聲明的方法和在原始類中的方法或該類(甚至是在一個(gè)父類)的其他 category 中的方法名稱相同,在運(yùn)行時(shí),編譯哪種方法的指令將被認(rèn)為是未定義的。 如果你正在使用你自己的類的 categories ,使用的 categories 會將方法添加到標(biāo)準(zhǔn)的 Cocoa 或 Cocoa Touch 類時(shí)導(dǎo)致問題。
例如當(dāng)你的應(yīng)用程序與遠(yuǎn)程 web 服務(wù)交互時(shí),可能需要一種使用 Base64 編碼技術(shù)來編碼字符串的方法。 因此你可以通過在 NSString 類上定義一個(gè) category ,添加一個(gè)稱為 base64EncodedString 的實(shí)例方法以返回一個(gè) Base64 編碼的字符串。
但是如果你鏈接到另一個(gè)框架,恰巧也在 NSString 類的自定義 category 中包括了此方法 也稱為 base64EncodedString 時(shí) ,那么將會出現(xiàn)問題。 在運(yùn)行時(shí),只有一個(gè)方法會“贏”,并添加到 NSString 類中,另一個(gè)則成為未定義不起作用。
如果你添加方法到 Cocoa 或 Cocoa Touch 類和之后版本的原始類中,那么可能會出現(xiàn)另一個(gè)問題。
例如 NSSortDescriptor 類,它描述了一個(gè)對象的集合應(yīng)該是如何排序的,包含有 aninitWithKey: accending 初始化方法。
但并沒有在早期的 OS X 和 iOS 版本下提供相應(yīng)的工廠類方法。
按照約定,工廠類方法應(yīng)該叫做 sortDescriptorWithKey: accending ,所以為方便起見你要選擇添加一個(gè) category 到 NSSortDescriptor 類上來提供此方法。 這是在舊版本的 OS X 和 iOS 下操作的,但隨著 Mac OS X 10.6 版本和 iOS 4.0 的發(fā)布,一個(gè)叫 sortDescriptorWithKey 的方法添加到原始的 NSSortDescriptor 類中,意味著在這些或更高版本操作系統(tǒng)上運(yùn)行你的應(yīng)用程序時(shí),你不再會有命名沖突的問題。
為了避免未定義的行為,最佳的做法是給框架類 categories 中的方法名添加一個(gè)前綴,就像你向你自己的類的名稱添加一個(gè)前綴一樣。
你可以選擇使用和你自己的類的前綴相同的三個(gè)字母,但要小寫以遵循方法命名的規(guī)則,然后在方法名稱的其余部分之間用一個(gè)下劃線連接。
對于 NSSortDescriptor 的示例,你的 category 應(yīng)該看起來像這樣:
@interface NSSortDescriptor (XYZAdditions)
(id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
這意味著你可以肯定你的方法在運(yùn)行時(shí)可以使用。歧義將會被刪除,你的代碼現(xiàn)在看起來像這樣:
NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];
類擴(kuò)展與 category 有相似性,但在編譯時(shí)它只能被添加到已有源代碼的一類中(該類擴(kuò)展和該類同時(shí)被編譯)。
聲明一個(gè)類擴(kuò)展的方法在原始類 @ implementation 塊中,所以你不能,舉個(gè)例子,在框架類上聲明一個(gè)類擴(kuò)展,如 Cocoa 或 Cocoa Touch 的 NSString 類。
用于聲明類擴(kuò)展的語法類似于一個(gè) category 聲明的語法,看起來像這樣:
@interface ClassName ()
@end
因?yàn)闆]有在括號內(nèi)給定名稱,所以類擴(kuò)展通常稱為匿名類。
不像一般的 categories ,類擴(kuò)展可以向類中添加其自己的屬性和成員變量。如果你在類擴(kuò)展中聲明一個(gè)屬性,要像這樣:
@interface XYZPerson ()
@property NSObject *extraProperty;
@end
編譯器會自動(dòng)合成相關(guān)的訪問方法,以及一個(gè)成員變量,繼承到主要的類。
如果你在一個(gè)類擴(kuò)展中添加任何方法,這些必須在主要類中繼承。
也可以使用一個(gè)類擴(kuò)展來添加自定義的成員變量。這些變量在類擴(kuò)展接口中的大括號內(nèi)聲明:
@interface XYZPerson () {
id _someCustomInstanceVariable;
}
...
@end
一個(gè)類的主要接口用于定義其他類將與之進(jìn)行交互的方式。換句話說,它是類的公共部分。
類擴(kuò)展通常用于擴(kuò)展額外的私有方法或?qū)傩缘墓步涌谝员阍陬惐旧淼膶?shí)現(xiàn)中使用。 例如,通常在界面中定義一個(gè)只讀屬性,但是為了在類的內(nèi)部方法可以直接更改屬性值,在繼承上層的一個(gè)類擴(kuò)展聲明中定義該屬性為讀寫屬性。
舉個(gè)例子,XYZPerson 類可以添加一個(gè)稱為 uniqueIdentifier 的屬性,用于跟蹤信息,比如在美國的社會安全號碼。
在現(xiàn)實(shí)世界中它通常需要大量的文書工作來給每一個(gè)人分配唯一的標(biāo)識符,所以 XYZPerson 類接口可能會聲明此屬性為只讀,并提供一些方法請求標(biāo)識符分配,像這樣:
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
這意味著 uniqueIdentifier 不可能直接由另一個(gè)對象設(shè)置。 如果一個(gè)人還未有一個(gè)唯一的標(biāo)識符,那么通過調(diào)用 assignUniqueIdentifier 方法將會作出分配一個(gè)標(biāo)識符的請求。
為了 XYZPerson 類能夠更改其內(nèi)部的屬性值,可以通過在類擴(kuò)展中重新定義在頂層類繼承的文件中被定義的屬性值來實(shí)現(xiàn):
@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end
注: 讀寫屬性是可選的,因?yàn)樗悄J(rèn)值。為清楚起見你可以在想使用它時(shí)重新聲明屬性。
這意味著編譯器現(xiàn)在將合成一個(gè) setter 方法,所以在 XYZPerson 類執(zhí)行內(nèi)部的任何方法都能夠直接使用 setter 方法或語法來設(shè)置該屬性值。 通過為 XYZPerson 類繼承的源代碼文件聲明類擴(kuò)展, 使得 XYZPerson 類的信息是私有的。 如果另一種類型的對象試圖設(shè)置該屬性時(shí),編譯器將生成一個(gè)錯(cuò)誤。
注: 如上所示通過添加類擴(kuò)展,重新定義 uniqueIdentifier 屬性為讀寫屬性,一個(gè)名為 setUniqueIdentifier: 的方法將在運(yùn)行時(shí)在每個(gè) XYZPerson 對象上存在,無論其他源代碼文件是否知道該類擴(kuò)展的存在。
當(dāng)其他源代碼文件中的某段代碼試圖調(diào)用一個(gè)私有方法或設(shè)置一個(gè)只讀屬性的值時(shí),編譯器會報(bào)錯(cuò),但利用動(dòng)態(tài)運(yùn)行功能使用其他方式調(diào)用這些方法是可以避免編譯器錯(cuò)誤的,例如通過使用由 NSObject 類提供的 performSelector 的方法。 你應(yīng)該避免出現(xiàn)一個(gè)類的層次結(jié)構(gòu)或者僅在必須的時(shí)候使用;相反主類接口應(yīng)始終定義正確的"公共接口"。
如果你打算在選擇其他類別時(shí),“私有”方法或?qū)傩匀允强捎玫?,例如在一個(gè)框架內(nèi)的相關(guān)類中。 你可以在單獨(dú)的頭文件中聲明一個(gè)類擴(kuò)展,并在需要它的源文件中導(dǎo)入它。在一個(gè)類中有兩個(gè)頭文件并不罕見,例如, XYZPerson.h 和 XYZPersonPrivate.h 等。 當(dāng)你釋放框架時(shí),你只需釋放公共的 XYZPerson.h 頭文件即可。
Categories 和類擴(kuò)展使得直接添加方法到一個(gè)現(xiàn)有的類變得很容易,但有時(shí)這并不是最好的選擇。
面向?qū)ο缶幊痰闹饕繕?biāo)之一是編寫可重用的代碼,這意味著在各種情況下所有的類都盡可能地被重復(fù)使用。
如果你正在創(chuàng)建一個(gè)視圖類來描述一個(gè)對象用于在屏幕上顯示信息,那考慮一下這個(gè)類能在多種情況下可用是必要的。
除了將關(guān)于布局或內(nèi)容的部分硬編碼,一種可選擇的方法是利用繼承并將這些部分留在方法中,特別是子類重寫的方法中。
雖然重用類并不會相對容易,因?yàn)槊看文阆胍褂玫哪莻€(gè)原始的類時(shí)你仍然需要?jiǎng)?chuàng)建一個(gè)新的子類。
另一種選擇是要對類使用一個(gè) delegate 對象。
任何可能會限制可重用性的部分都可授權(quán)給另一個(gè)對象,也就是說可以在運(yùn)行時(shí)編譯這些部分。 一個(gè)常見的例子是標(biāo)準(zhǔn)表視圖類 ( OS X 的 NSTableView 和 iOS 的 UITableView )。 為了使一般表格視圖 (使用一個(gè)或多個(gè)列和行顯示信息的對象)可用,它將內(nèi)容部分留給另一個(gè)對象在運(yùn)行時(shí)決定。 在下一章 Working with Protocols 中會詳細(xì)的介紹如何使用授權(quán) 。
Objective-C 通過其運(yùn)行庫系統(tǒng)提供動(dòng)態(tài)功能。
許多決定并不在編譯時(shí)作出,而在應(yīng)用程序運(yùn)行時(shí)決定,例如哪些方法調(diào)用時(shí)會發(fā)送消息的決定。 Objective- C 不僅僅是一種編譯機(jī)器的語言代碼,而且它還需要一個(gè)運(yùn)行庫系統(tǒng)來執(zhí)行代碼。
它是可以直接與運(yùn)行庫系統(tǒng)進(jìn)行交互的,例如給對象添加關(guān)聯(lián)引用。 不同于類擴(kuò)展,關(guān)聯(lián)引用不會影響原始類的聲明和繼承,這意味著你可以將它們用于你沒有權(quán)限訪問的原始源代碼的框架類。
一個(gè)關(guān)聯(lián)引用是用來鏈接兩個(gè)對象的,類似于一個(gè)屬性和成員變量。 獲取更多的信息,請參閱關(guān)聯(lián)引用部分。若要了解更多有關(guān) Objective-C 的內(nèi)容,請參閱 Objective-C Runtime Programming Guide。
添加一個(gè) category 到 XYZPerson 類來聲明和繼承附加的功能,例如以不同的方式顯示一個(gè)人的名字。
向 NSString 類 添加一個(gè) category,以添加一個(gè)方法來在給定位置繪制全部字母大寫的字符串,通過調(diào)用到一個(gè)現(xiàn)有的 NSStringDrawing category 方法來執(zhí)行實(shí)際的繪制。
這些方法都記錄在 iOS 的 NSString UIKit Additions Reference 中和 OSX 的 NSString Application Kit Additions Reference中。
更多建議: