鍍金池/ 教程/ iOS/ 自定義現(xiàn)有的類 - Customizing Existing Classes
使用對象 - Working with Objects
使用塊 - Working with Blocks
自定義現(xiàn)有的類 - Customizing Existing Classes
使用協(xié)議 - Working with Protocols
定義類 - Defining Classes
賦值與集合 - Values and Collections
關于 Objective-C
命名規(guī)則 - Conventions
錯誤處理 - Dealing with Errors
數(shù)據(jù)封裝 - Encapsulating Data

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

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

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

在這種情況下,對原始的、主要的類的接口增加功能方法并不總是可行的。 因為在大多數(shù)應用字符串對象的程序中,繪圖能力并不總是被要求的。 例如,在 NSString 類中,你不能修改原來的接口或是繼承,因為它是一個框架類。

此外,上述方法對現(xiàn)有類的子類也是沒有意義的,因為你可能希望你的繪圖能力不僅對原始的 NSString 類有效,也有對該類子類有效,如 NSMutableString 類。 另外雖然 NSString 類在 OS X 和 iOS 兩個操作系統(tǒng)內均可使用,但相關繪圖能力的代碼在每個操作系統(tǒng)內是不同的,所以你需要在每個操作系統(tǒng)內使用不同的子類。

然而,Objective-C 允許你通過 categories 和類擴展來對已有的類中添加你自定義的方法。

使用 Categories 對現(xiàn)有的類添加方法

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

使用 @ interface 關鍵字來聲明一個 category ,就像標準的 Objective-C 類的描述一樣,但并不表示這個 category 從任何一個子類繼承。 另外它指定 category 的名稱在括號內,像這樣:

   @interface ClassName (CategoryName)

   @end

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

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

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

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

#import "XYZPerson.h"

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

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

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

雖然用 category 添加的任何方法都可用于此類及其子類的所有實例中,但你仍需要在任何要使用添加的方法的源代碼文件中導入含有 category 的頭文件,否則你可能會遇到編譯器警告和錯誤。

Category 的實現(xiàn)如下所示:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"

@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end

一旦你已經聲明一個 category 并繼承這些方法,你可以在此類的任何實例中使用這些方法,就好像他們是原始類接口的一部分一樣:

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

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

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

注: Cocoa 和 Cocoa Touch 包括了大量的主要框架類的 categories。

這一章的導言中提到的字符串繪圖功能事實上已經由 OS X 中名為 NSStringDrawing 的 category 提供給 NSString 類了,其中包括 drawAtPoint:withAttributes: 和 drawInRect:withAttributes: 方法。 對于 iOS ,UIStringDrawing category 包括 drawAtPoint: withFont 方法和 drawInRect: withFont 方法。

避免 categories 方法名沖突

因為在一個 category 中聲明的方法已經添加到現(xiàn)有的類中,所以你需要非常小心有關方法名的定義問題。

如果在一個 category 中聲明的方法和在原始類中的方法或該類(甚至是在一個父類)的其他 category 中的方法名稱相同,在運行時,編譯哪種方法的指令將被認為是未定義的。 如果你正在使用你自己的類的 categories ,使用的 categories 會將方法添加到標準的 Cocoa 或 Cocoa Touch 類時導致問題。

例如當你的應用程序與遠程 web 服務交互時,可能需要一種使用 Base64 編碼技術來編碼字符串的方法。 因此你可以通過在 NSString 類上定義一個 category ,添加一個稱為 base64EncodedString 的實例方法以返回一個 Base64 編碼的字符串。

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

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

例如 NSSortDescriptor 類,它描述了一個對象的集合應該是如何排序的,包含有 aninitWithKey: accending 初始化方法。

但并沒有在早期的 OS X 和 iOS 版本下提供相應的工廠類方法。

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

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

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

對于 NSSortDescriptor 的示例,你的 category 應該看起來像這樣:

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

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

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

用 extension 來實現(xiàn)類的擴展

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

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

用于聲明類擴展的語法類似于一個 category 聲明的語法,看起來像這樣:

@interface ClassName ()

@end

因為沒有在括號內給定名稱,所以類擴展通常稱為匿名類。

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

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

編譯器會自動合成相關的訪問方法,以及一個成員變量,繼承到主要的類。

如果你在一個類擴展中添加任何方法,這些必須在主要類中繼承。

也可以使用一個類擴展來添加自定義的成員變量。這些變量在類擴展接口中的大括號內聲明:

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

使用類擴展來隱藏私有信息

一個類的主要接口用于定義其他類將與之進行交互的方式。換句話說,它是類的公共部分。

類擴展通常用于擴展額外的私有方法或屬性的公共接口以便在類本身的實現(xiàn)中使用。 例如,通常在界面中定義一個只讀屬性,但是為了在類的內部方法可以直接更改屬性值,在繼承上層的一個類擴展聲明中定義該屬性為讀寫屬性。

舉個例子,XYZPerson 類可以添加一個稱為 uniqueIdentifier 的屬性,用于跟蹤信息,比如在美國的社會安全號碼。

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

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

這意味著 uniqueIdentifier 不可能直接由另一個對象設置。 如果一個人還未有一個唯一的標識符,那么通過調用 assignUniqueIdentifier 方法將會作出分配一個標識符的請求。

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

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

@implementation XYZPerson
...
@end

注: 讀寫屬性是可選的,因為它是默認值。為清楚起見你可以在想使用它時重新聲明屬性。

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

注: 如上所示通過添加類擴展,重新定義 uniqueIdentifier 屬性為讀寫屬性,一個名為 setUniqueIdentifier: 的方法將在運行時在每個 XYZPerson 對象上存在,無論其他源代碼文件是否知道該類擴展的存在。

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

如果你打算在選擇其他類別時,“私有”方法或屬性仍是可用的,例如在一個框架內的相關類中。 你可以在單獨的頭文件中聲明一個類擴展,并在需要它的源文件中導入它。在一個類中有兩個頭文件并不罕見,例如, XYZPerson.h 和 XYZPersonPrivate.h 等。 當你釋放框架時,你只需釋放公共的 XYZPerson.h 頭文件即可。

考慮其他辦法來自定義類

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

面向對象編程的主要目標之一是編寫可重用的代碼,這意味著在各種情況下所有的類都盡可能地被重復使用。

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

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

雖然重用類并不會相對容易,因為每次你想要使用的那個原始的類時你仍然需要創(chuàng)建一個新的子類。

另一種選擇是要對類使用一個 delegate 對象。

任何可能會限制可重用性的部分都可授權給另一個對象,也就是說可以在運行時編譯這些部分。 一個常見的例子是標準表視圖類 ( OS X 的 NSTableView 和 iOS 的 UITableView )。 為了使一般表格視圖 (使用一個或多個列和行顯示信息的對象)可用,它將內容部分留給另一個對象在運行時決定。 在下一章 Working with Protocols 中會詳細的介紹如何使用授權 。

直接與 Objective-C 運行庫進行交互

Objective-C 通過其運行庫系統(tǒng)提供動態(tài)功能。

許多決定并不在編譯時作出,而在應用程序運行時決定,例如哪些方法調用時會發(fā)送消息的決定。 Objective- C 不僅僅是一種編譯機器的語言代碼,而且它還需要一個運行庫系統(tǒng)來執(zhí)行代碼。

它是可以直接與運行庫系統(tǒng)進行交互的,例如給對象添加關聯(lián)引用。 不同于類擴展,關聯(lián)引用不會影響原始類的聲明和繼承,這意味著你可以將它們用于你沒有權限訪問的原始源代碼的框架類。

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

練習

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

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

  3. 將兩個只讀屬性添加到原始 XYZPerson 類的繼承中,來代表一個人的身高和體重,分別是 measureWeight 和 measureHeight 方法。
    使用類擴展重新聲明屬性為讀寫屬性,并繼承以便將屬性設置為適當?shù)闹怠?/li>