自定義現(xiàn)有的類(lèi) - Customizing Existing Classes

2018-08-12 21:19 更新

自定義現(xiàn)有的類(lèi) - Customizing Existing Classes

每一個(gè)對(duì)象都應(yīng)該有明確的任務(wù),如為具體信息建模,顯示可視化內(nèi)容或者控制信息流。 另外正如你所知道的,一個(gè)類(lèi)的接口定義了其利用一個(gè)對(duì)象來(lái)幫助它完成任務(wù)的連接方式。

有時(shí)你會(huì)發(fā)現(xiàn)你希望通過(guò)添加某些方法來(lái)擴(kuò)展現(xiàn)有的類(lèi),但這只在某些情況下是有用的。 舉個(gè)例子,你發(fā)現(xiàn)你的應(yīng)用程序經(jīng)常需要在一個(gè)可視化界面顯示一些字符串信息。 那么除了在每次你需要顯示字符串時(shí)創(chuàng)造一個(gè)字符串繪圖對(duì)象來(lái)使用外,給 NSString 類(lèi)自己本身賦予可以在屏幕上繪制字符的能力會(huì)更有意義。

在這種情況下,對(duì)原始的、主要的類(lèi)的接口增加功能方法并不總是可行的。 因?yàn)樵诖蠖鄶?shù)應(yīng)用字符串對(duì)象的程序中,繪圖能力并不總是被要求的。 例如,在 NSString 類(lèi)中,你不能修改原來(lái)的接口或是繼承,因?yàn)樗且粋€(gè)框架類(lèi)。

此外,上述方法對(duì)現(xiàn)有類(lèi)的子類(lèi)也是沒(méi)有意義的,因?yàn)槟憧赡芟M愕睦L圖能力不僅對(duì)原始的 NSString 類(lèi)有效,也有對(duì)該類(lèi)子類(lèi)有效,如 NSMutableString 類(lèi)。 另外雖然 NSString 類(lèi)在 OS X 和 iOS 兩個(gè)操作系統(tǒng)內(nèi)均可使用,但相關(guān)繪圖能力的代碼在每個(gè)操作系統(tǒng)內(nèi)是不同的,所以你需要在每個(gè)操作系統(tǒng)內(nèi)使用不同的子類(lèi)。

然而,Objective-C 允許你通過(guò) categories 和類(lèi)擴(kuò)展來(lái)對(duì)已有的類(lèi)中添加你自定義的方法。

使用 Categories 對(duì)現(xiàn)有的類(lèi)添加方法

如果你需要添加方法到現(xiàn)有類(lèi),是為了添加某些功能使你自己的應(yīng)用程序在完成某些人任務(wù)時(shí)更容易,那么使用 category 是最方便的方法。

使用 @ interface 關(guān)鍵字來(lái)聲明一個(gè) category ,就像標(biāo)準(zhǔn)的 Objective-C 類(lèi)的描述一樣,但并不表示這個(gè) category 從任何一個(gè)子類(lèi)繼承。 另外它指定 category 的名稱(chēng)在括號(hào)內(nèi),像這樣:

   @interface ClassName (CategoryName)

   @end

一個(gè) category 可以聲明任何類(lèi),即使是在沒(méi)有原始代碼的類(lèi)(如標(biāo)準(zhǔn)的 Cocoa 或 Cocoa Touch 的類(lèi))。 你在 category 中聲明的任何方法都可以被原始類(lèi)和任何原始類(lèi)的子類(lèi)所實(shí)例化。 同時(shí)在運(yùn)行時(shí),你在 category 里添加的方法和由原始類(lèi)實(shí)現(xiàn)的方法之間是沒(méi)有區(qū)別的。

請(qǐng)考慮在之前章節(jié)提過(guò)的 XYZPerson 類(lèi),它具有一個(gè)人的姓氏和名字的屬性。 如果你在寫(xiě)一個(gè)記錄的應(yīng)用程序,你會(huì)發(fā)現(xiàn)你經(jīng)常需要顯示一個(gè)按姓氏排列的名單,像這樣:

Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate

如果你不想在每次顯示這個(gè)列表的時(shí)候再編寫(xiě)代碼來(lái)生成適當(dāng)?shù)?lastName , firstName 字符串, 那么你可以向 XYZPerson 類(lèi)如下所示添加一個(gè) category :

#import "XYZPerson.h"

@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end

在此示例中,名為 XYZPersonNameDisplayAdditions 的 category 聲明了一個(gè)額外的方法以返回必需的字符串。

一個(gè) category 通常是在單獨(dú)的頭文件中聲明的,并在單獨(dú)的源代碼文件被實(shí)現(xiàn)。 例如在 XYZPerson 類(lèi)中,你可能會(huì)聲明一個(gè) category 在 XYZPerson+XYZPersonNameDisplayAdditions.h 的頭文件中。

雖然用 category 添加的任何方法都可用于此類(lèi)及其子類(lèi)的所有實(shí)例中,但你仍需要在任何要使用添加的方法的源代碼文件中導(dǎo)入含有 category 的頭文件,否則你可能會(huì)遇到編譯器警告和錯(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 并繼承這些方法,你可以在此類(lèi)的任何實(shí)例中使用這些方法,就好像他們是原始類(lèi)接口的一部分一樣:

#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)有的類(lèi)添加方法,你還可以使用 categories 把多功能的源代碼文件中一個(gè)復(fù)雜的類(lèi)拆分。 例如,你把一個(gè)自定義用戶界面元素的繪圖代碼放在一個(gè)單獨(dú)的 category 中,來(lái)執(zhí)行一些其余的功能如幾何計(jì)算、 顏色和漸變等,這就是一個(gè)特別復(fù)雜的類(lèi)的例子。 另外你可以給類(lèi)別的方法提供不同的實(shí)現(xiàn),具體取決于你正在寫(xiě)一個(gè) OS X 還是 iOS 的應(yīng)用程序。

Categories 可用于聲明類(lèi)方法或成員方法,但并非通常適合聲明附加屬性。 在一個(gè) category 的接口中包含屬性聲明時(shí)編譯器不會(huì)報(bào)錯(cuò),但是不能在一個(gè) category 中聲明一個(gè)附加的成員變量。 這意味著,編譯器不會(huì)為該屬性合成任何成員變量,也不合成任何屬性訪問(wèn)方法。 在類(lèi)的實(shí)現(xiàn)過(guò)程中,你可以編寫(xiě)你自己的訪問(wèn)方法,但是你不能來(lái)跟蹤該屬性的值,除非原始類(lèi)中已有了該成員變量。

添加一個(gè)傳統(tǒng)屬性的唯一方式——也就是從現(xiàn)有類(lèi)支持一個(gè)新的成員變量——是使用類(lèi)擴(kuò)展,如 Class Extensions Extend the Internal Implementation。

注: Cocoa 和 Cocoa Touch 包括了大量的主要框架類(lèi)的 categories。

這一章的導(dǎo)言中提到的字符串繪圖功能事實(shí)上已經(jīng)由 OS X 中名為 NSStringDrawing 的 category 提供給 NSString 類(lèi)了,其中包括 drawAtPoint:withAttributes: 和 drawInRect:withAttributes: 方法。 對(duì)于 iOS ,UIStringDrawing category 包括 drawAtPoint: withFont 方法和 drawInRect: withFont 方法。

避免 categories 方法名沖突

因?yàn)樵谝粋€(gè) category 中聲明的方法已經(jīng)添加到現(xiàn)有的類(lèi)中,所以你需要非常小心有關(guān)方法名的定義問(wèn)題。

如果在一個(gè) category 中聲明的方法和在原始類(lèi)中的方法或該類(lèi)(甚至是在一個(gè)父類(lèi))的其他 category 中的方法名稱(chēng)相同,在運(yùn)行時(shí),編譯哪種方法的指令將被認(rèn)為是未定義的。 如果你正在使用你自己的類(lèi)的 categories ,使用的 categories 會(huì)將方法添加到標(biāo)準(zhǔn)的 Cocoa 或 Cocoa Touch 類(lèi)時(shí)導(dǎo)致問(wèn)題。

例如當(dāng)你的應(yīng)用程序與遠(yuǎn)程 web 服務(wù)交互時(shí),可能需要一種使用 Base64 編碼技術(shù)來(lái)編碼字符串的方法。 因此你可以通過(guò)在 NSString 類(lèi)上定義一個(gè) category ,添加一個(gè)稱(chēng)為 base64EncodedString 的實(shí)例方法以返回一個(gè) Base64 編碼的字符串。

但是如果你鏈接到另一個(gè)框架,恰巧也在 NSString 類(lèi)的自定義 category 中包括了此方法 也稱(chēng)為 base64EncodedString 時(shí) ,那么將會(huì)出現(xiàn)問(wèn)題。 在運(yùn)行時(shí),只有一個(gè)方法會(huì)“贏”,并添加到 NSString 類(lèi)中,另一個(gè)則成為未定義不起作用。

如果你添加方法到 Cocoa 或 Cocoa Touch 類(lèi)和之后版本的原始類(lèi)中,那么可能會(huì)出現(xiàn)另一個(gè)問(wèn)題。

例如 NSSortDescriptor 類(lèi),它描述了一個(gè)對(duì)象的集合應(yīng)該是如何排序的,包含有 aninitWithKey: accending 初始化方法。

但并沒(méi)有在早期的 OS X 和 iOS 版本下提供相應(yīng)的工廠類(lèi)方法。

按照約定,工廠類(lèi)方法應(yīng)該叫做 sortDescriptorWithKey: accending ,所以為方便起見(jiàn)你要選擇添加一個(gè) category 到 NSSortDescriptor 類(lèi)上來(lái)提供此方法。 這是在舊版本的 OS X 和 iOS 下操作的,但隨著 Mac OS X 10.6 版本和 iOS 4.0 的發(fā)布,一個(gè)叫 sortDescriptorWithKey 的方法添加到原始的 NSSortDescriptor 類(lèi)中,意味著在這些或更高版本操作系統(tǒng)上運(yùn)行你的應(yīng)用程序時(shí),你不再會(huì)有命名沖突的問(wèn)題。

為了避免未定義的行為,最佳的做法是給框架類(lèi) categories 中的方法名添加一個(gè)前綴,就像你向你自己的類(lèi)的名稱(chēng)添加一個(gè)前綴一樣。

你可以選擇使用和你自己的類(lèi)的前綴相同的三個(gè)字母,但要小寫(xiě)以遵循方法命名的規(guī)則,然后在方法名稱(chēng)的其余部分之間用一個(gè)下劃線連接。

對(duì)于 NSSortDescriptor 的示例,你的 category 應(yīng)該看起來(lái)像這樣:

@interface NSSortDescriptor (XYZAdditions)
 (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

這意味著你可以肯定你的方法在運(yùn)行時(shí)可以使用。歧義將會(huì)被刪除,你的代碼現(xiàn)在看起來(lái)像這樣:

    NSSortDescriptor *descriptor =
               [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];

用 extension 來(lái)實(shí)現(xiàn)類(lèi)的擴(kuò)展

類(lèi)擴(kuò)展與 category 有相似性,但在編譯時(shí)它只能被添加到已有源代碼的一類(lèi)中(該類(lèi)擴(kuò)展和該類(lèi)同時(shí)被編譯)。

聲明一個(gè)類(lèi)擴(kuò)展的方法在原始類(lèi) @ implementation 塊中,所以你不能,舉個(gè)例子,在框架類(lèi)上聲明一個(gè)類(lèi)擴(kuò)展,如 Cocoa 或 Cocoa Touch 的 NSString 類(lèi)。

用于聲明類(lèi)擴(kuò)展的語(yǔ)法類(lèi)似于一個(gè) category 聲明的語(yǔ)法,看起來(lái)像這樣:

@interface ClassName ()

@end

因?yàn)闆](méi)有在括號(hào)內(nèi)給定名稱(chēng),所以類(lèi)擴(kuò)展通常稱(chēng)為匿名類(lèi)。

不像一般的 categories ,類(lèi)擴(kuò)展可以向類(lèi)中添加其自己的屬性和成員變量。如果你在類(lèi)擴(kuò)展中聲明一個(gè)屬性,要像這樣:

@interface XYZPerson ()
@property NSObject *extraProperty;
@end

編譯器會(huì)自動(dòng)合成相關(guān)的訪問(wèn)方法,以及一個(gè)成員變量,繼承到主要的類(lèi)。

如果你在一個(gè)類(lèi)擴(kuò)展中添加任何方法,這些必須在主要類(lèi)中繼承。

也可以使用一個(gè)類(lèi)擴(kuò)展來(lái)添加自定義的成員變量。這些變量在類(lèi)擴(kuò)展接口中的大括號(hào)內(nèi)聲明:

@interface XYZPerson () {
    id _someCustomInstanceVariable;
}
...
@end

使用類(lèi)擴(kuò)展來(lái)隱藏私有信息

一個(gè)類(lèi)的主要接口用于定義其他類(lèi)將與之進(jìn)行交互的方式。換句話說(shuō),它是類(lèi)的公共部分。

類(lèi)擴(kuò)展通常用于擴(kuò)展額外的私有方法或?qū)傩缘墓步涌谝员阍陬?lèi)本身的實(shí)現(xiàn)中使用。 例如,通常在界面中定義一個(gè)只讀屬性,但是為了在類(lèi)的內(nèi)部方法可以直接更改屬性值,在繼承上層的一個(gè)類(lèi)擴(kuò)展聲明中定義該屬性為讀寫(xiě)屬性。

舉個(gè)例子,XYZPerson 類(lèi)可以添加一個(gè)稱(chēng)為 uniqueIdentifier 的屬性,用于跟蹤信息,比如在美國(guó)的社會(huì)安全號(hào)碼。

在現(xiàn)實(shí)世界中它通常需要大量的文書(shū)工作來(lái)給每一個(gè)人分配唯一的標(biāo)識(shí)符,所以 XYZPerson 類(lèi)接口可能會(huì)聲明此屬性為只讀,并提供一些方法請(qǐng)求標(biāo)識(shí)符分配,像這樣:

@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end

這意味著 uniqueIdentifier 不可能直接由另一個(gè)對(duì)象設(shè)置。 如果一個(gè)人還未有一個(gè)唯一的標(biāo)識(shí)符,那么通過(guò)調(diào)用 assignUniqueIdentifier 方法將會(huì)作出分配一個(gè)標(biāo)識(shí)符的請(qǐng)求。

為了 XYZPerson 類(lèi)能夠更改其內(nèi)部的屬性值,可以通過(guò)在類(lèi)擴(kuò)展中重新定義在頂層類(lèi)繼承的文件中被定義的屬性值來(lái)實(shí)現(xiàn):

@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end

@implementation XYZPerson
...
@end

注: 讀寫(xiě)屬性是可選的,因?yàn)樗悄J(rèn)值。為清楚起見(jiàn)你可以在想使用它時(shí)重新聲明屬性。

這意味著編譯器現(xiàn)在將合成一個(gè) setter 方法,所以在 XYZPerson 類(lèi)執(zhí)行內(nèi)部的任何方法都能夠直接使用 setter 方法或語(yǔ)法來(lái)設(shè)置該屬性值。 通過(guò)為 XYZPerson 類(lèi)繼承的源代碼文件聲明類(lèi)擴(kuò)展, 使得 XYZPerson 類(lèi)的信息是私有的。 如果另一種類(lèi)型的對(duì)象試圖設(shè)置該屬性時(shí),編譯器將生成一個(gè)錯(cuò)誤。

注: 如上所示通過(guò)添加類(lèi)擴(kuò)展,重新定義 uniqueIdentifier 屬性為讀寫(xiě)屬性,一個(gè)名為 setUniqueIdentifier: 的方法將在運(yùn)行時(shí)在每個(gè) XYZPerson 對(duì)象上存在,無(wú)論其他源代碼文件是否知道該類(lèi)擴(kuò)展的存在。

當(dāng)其他源代碼文件中的某段代碼試圖調(diào)用一個(gè)私有方法或設(shè)置一個(gè)只讀屬性的值時(shí),編譯器會(huì)報(bào)錯(cuò),但利用動(dòng)態(tài)運(yùn)行功能使用其他方式調(diào)用這些方法是可以避免編譯器錯(cuò)誤的,例如通過(guò)使用由 NSObject 類(lèi)提供的 performSelector 的方法。 你應(yīng)該避免出現(xiàn)一個(gè)類(lèi)的層次結(jié)構(gòu)或者僅在必須的時(shí)候使用;相反主類(lèi)接口應(yīng)始終定義正確的"公共接口"。

如果你打算在選擇其他類(lèi)別時(shí),“私有”方法或?qū)傩匀允强捎玫?,例如在一個(gè)框架內(nèi)的相關(guān)類(lèi)中。 你可以在單獨(dú)的頭文件中聲明一個(gè)類(lèi)擴(kuò)展,并在需要它的源文件中導(dǎo)入它。在一個(gè)類(lèi)中有兩個(gè)頭文件并不罕見(jiàn),例如, XYZPerson.h 和 XYZPersonPrivate.h 等。 當(dāng)你釋放框架時(shí),你只需釋放公共的 XYZPerson.h 頭文件即可。

考慮其他辦法來(lái)自定義類(lèi)

Categories 和類(lèi)擴(kuò)展使得直接添加方法到一個(gè)現(xiàn)有的類(lèi)變得很容易,但有時(shí)這并不是最好的選擇。

面向?qū)ο缶幊痰闹饕繕?biāo)之一是編寫(xiě)可重用的代碼,這意味著在各種情況下所有的類(lèi)都盡可能地被重復(fù)使用。

如果你正在創(chuàng)建一個(gè)視圖類(lèi)來(lái)描述一個(gè)對(duì)象用于在屏幕上顯示信息,那考慮一下這個(gè)類(lèi)能在多種情況下可用是必要的。

除了將關(guān)于布局或內(nèi)容的部分硬編碼,一種可選擇的方法是利用繼承并將這些部分留在方法中,特別是子類(lèi)重寫(xiě)的方法中。

雖然重用類(lèi)并不會(huì)相對(duì)容易,因?yàn)槊看文阆胍褂玫哪莻€(gè)原始的類(lèi)時(shí)你仍然需要?jiǎng)?chuàng)建一個(gè)新的子類(lèi)。

另一種選擇是要對(duì)類(lèi)使用一個(gè) delegate 對(duì)象。

任何可能會(huì)限制可重用性的部分都可授權(quán)給另一個(gè)對(duì)象,也就是說(shuō)可以在運(yùn)行時(shí)編譯這些部分。 一個(gè)常見(jiàn)的例子是標(biāo)準(zhǔn)表視圖類(lèi) ( OS X 的 NSTableView 和 iOS 的 UITableView )。 為了使一般表格視圖 (使用一個(gè)或多個(gè)列和行顯示信息的對(duì)象)可用,它將內(nèi)容部分留給另一個(gè)對(duì)象在運(yùn)行時(shí)決定。 在下一章 Working with Protocols 中會(huì)詳細(xì)的介紹如何使用授權(quán) 。

直接與 Objective-C 運(yùn)行庫(kù)進(jìn)行交互

Objective-C 通過(guò)其運(yùn)行庫(kù)系統(tǒng)提供動(dòng)態(tài)功能。

許多決定并不在編譯時(shí)作出,而在應(yīng)用程序運(yùn)行時(shí)決定,例如哪些方法調(diào)用時(shí)會(huì)發(fā)送消息的決定。 Objective- C 不僅僅是一種編譯機(jī)器的語(yǔ)言代碼,而且它還需要一個(gè)運(yùn)行庫(kù)系統(tǒng)來(lái)執(zhí)行代碼。

它是可以直接與運(yùn)行庫(kù)系統(tǒng)進(jìn)行交互的,例如給對(duì)象添加關(guān)聯(lián)引用。 不同于類(lèi)擴(kuò)展,關(guān)聯(lián)引用不會(huì)影響原始類(lèi)的聲明和繼承,這意味著你可以將它們用于你沒(méi)有權(quán)限訪問(wèn)的原始源代碼的框架類(lèi)。

一個(gè)關(guān)聯(lián)引用是用來(lái)鏈接兩個(gè)對(duì)象的,類(lèi)似于一個(gè)屬性和成員變量。 獲取更多的信息,請(qǐng)參閱關(guān)聯(lián)引用部分。若要了解更多有關(guān) Objective-C 的內(nèi)容,請(qǐng)參閱 Objective-C Runtime Programming Guide。

練習(xí)

  1. 添加一個(gè) category 到 XYZPerson 類(lèi)來(lái)聲明和繼承附加的功能,例如以不同的方式顯示一個(gè)人的名字。

  2. 向 NSString 類(lèi) 添加一個(gè) category,以添加一個(gè)方法來(lái)在給定位置繪制全部字母大寫(xiě)的字符串,通過(guò)調(diào)用到一個(gè)現(xiàn)有的 NSStringDrawing category 方法來(lái)執(zhí)行實(shí)際的繪制。
    這些方法都記錄在 iOS 的 NSString UIKit Additions Reference 中和 OSX 的 NSString Application Kit Additions Reference中。

  3. 將兩個(gè)只讀屬性添加到原始 XYZPerson 類(lèi)的繼承中,來(lái)代表一個(gè)人的身高和體重,分別是 measureWeight 和 measureHeight 方法。
    使用類(lèi)擴(kuò)展重新聲明屬性為讀寫(xiě)屬性,并繼承以便將屬性設(shè)置為適當(dāng)?shù)闹怠?/li>
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)