iOS開發讀書筆記:Effective Objective-C 2.0 52個有效方法 - 篇2/4

iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇1/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇2/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇3/4
iOS開發讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇4/4

  • 第二章 對象、消息、運行期
    • 第11條:理解objc_msgSend的作用
    • 第12條:理解消息轉發機制
    • 第13條:用“方法調配技術”調試“黑盒方法”
    • 第14條:理解“類對象”的用意
  • 第三章 接口與API設計
    • 第15條:用前綴避免命名空間沖突
    • 第16條:提供“全能初始化方法”
    • 第17條:實現description方法
    • 第18條:盡量使用不可變對象
    • 第19條:使用清晰而協調的命名方式
    • 第20條:為私有方法名加前綴
    • 第21條:理解Objective-C錯誤模型
    • 第22條:理解NSCopying協議
  • 第四章 協議與分類
    • 第23條:通過委托與數據源協議進行對象間通信
    • 第24條:將類的實現代碼分散到便于管理的數個分類當中
    • 第25條:總是為第三方類的分類名稱加前綴
    • 第26條:勿在分類中聲明屬性
    • 第27條:使用"class - continuation"分類隱藏實現細節
    • 第28條:通過協議提供匿名對象

第11條:理解objc_msgSend的作用

在對象上調用方法是Objective-C中經常使用的功能。用Objective-C的術語來說,這叫做“傳遞消息”(pass a message)。
由于Objective-C是C的超集,所以最好先理解C語言的函數調用方式。C語言使用“靜態綁定”(static binding),也就是說,在編譯期就能決定運行時所應調用的函數。

import <stdio.h>

void printHello {
  printf ("Hello, world! \n");
}

void printGoodbye() {
  printf ("Goodbye, world! \n");
}

void doTheThing(int type) { 
  if (type == 0) {
  printHello();
} else  {
  printGoodbye();
}
  return 0;
}

如果不考慮“內聯”(inline),那么編譯器在編譯代碼的時候就已經知道程序中有該函數了,于是會直接生成調用這些函數的指令。而函數地址實際上是硬編碼在指令之中的。若是將剛才那段代碼寫成下面這樣,會如何?

void doTheThing(int type) { 
  void (*fnc)(); 
  if (type == 0) {
    fnc = printHello;
  } else {
    fnc = printGoodbye;
  }
  fnc (); 
  return 0;
}

這時就得使用“動態綁定”(dynamic binding) 了,因為所要調用的函數直到運行期才能確定。編譯器在這種情況下生成的指令與剛才那個例子不同,在第一個例子中,if與else語句里都有函數調用指令。而在第二個例子中,只有一個函數調用指令,不過待調用的函數地址無法硬編碼在指令之中,而是要在運行期讀取出來。

在Objective-C中,如果向某對象傳遞消息,那就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通的C語言函數,然而對象收到消息之后,究竟該調用哪個方法則完全于運行期決定,甚至可以在程序運行時改變,這些特性使得Objective-C成為一門真正的動態語言。
給對象發送消息可以這樣來寫:

id returnValue = [someObject messageName:parameter];

在本例中,someObject 叫做“接收者”(receiver), messageName 叫做“選擇子”(selector),選擇子指的就是方法的名字, “選擇子”與“方法”這兩個詞經常交替使用。選擇子與參數合起來稱為“消息”(message)。編譯器看到此消息后,將其轉換為一條標準的C語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數,叫做objc_msgSend,其 “原型"(prototype)如下:

void objc_msgSend(id self, SEL cmd,...)

編譯器會把剛才那個例子中的消息轉換為如下函數:

id returnValue = objc_msgSend(someObject,@selector(messageName:), parameter);

每個類里都有一張表格,其中的指針都會指向這種函數,而選擇子的名稱則是査表時所用的“鍵”。objC_mSgSend等函數正是通過這張表格來尋找應該執行的方法并跳至其實現的。objc_msgSend函數會依據接收者與選擇子的類型來調用適當的方法。為了完成此操作, 該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若是找不到,那就沿著繼承體系繼續向上査找,等找到合適的方法之后再跳轉。如果最終還是找不到相符的方法,那就執行“消息轉發” (message forwarding)操作。

這么說來,想調用一個方法似乎需要很多步驟。所幸objc_mSgSend會將匹配結果緩存在“快速映射表"(fast map)里面,每個類都有這樣一塊緩存,若是稍后還向該類發送與選擇子相同的消息,那么執行起來就很快了。當然這種“快速執行路徑”(fastpath)還是不如 “靜態綁定的函數調用操作’(statically bound function call)那樣迅速,不過只要把選擇子緩存起來,也就不會慢很多,實際上,消息派發(message dispatch)并非應用程序的瓶頸所在。假如真是個瓶頸的話,那你可以只編寫純C函數,在調用時根據需要,把ObjectWe-C對象的狀態傳進去。

前面講的這部分內容只描述了部分消息的調用過程,其他“邊界情況"(edgecase)。則需要交由Objective-C運行環境中的另一些函數來處理:

  1. objc_msgSend_stret:如果待發送的消息要返回結構體,那么可交由此函數處理。只有當CPU的寄存器能夠容納得下消息返回類型時,這個函數才能處理此消息。若是返回值無法容納于CPU寄存器中(比如說返回的結構體太大了),那么就由另一個函數執行派發。此時,那個函數會通過分配在棧上的某個變量來處理消息所返回的結構體。
  2. objc_mSgSend_fpret:如果消息返回的是浮點數,那么可交由此函數處理。在某些架構的CPU中調用函數時,需要對“浮點數寄存器’(floating-point register)做特殊處理, 也就是說,通常所用的objC_msgSend在這種情況下并不合適。這個函數是為了處理 x86等架構CPU中某些令人稍覺驚訝的奇怪狀況。
  3. objc_msgSendSuper :如果要給超類發消息,例如[super message:parameter],那么就交由此函數處理。也有另外兩個與objc_msgSend_stret和objc_msgSend_fpret等效的函數,用于處理發給super的相應消息。

利用“尾調用優化”技術,令“跳至方法實現”這一操作變得更簡單些。

如果某函數的最后一項操作是調用另外一個函數,那么就可以運用“尾調用優化”技術。 編譯器會生成調轉至另一函數所需的指令碼,而且不會向調用堆棧中推入新的“棧幀"(frame stack)。只有當某函數的最后一個操作僅僅是調用其他函數而不會將其返回值另作他用時, 才能執行“尾調用優化”。這項優化對objc_mSgSend非常關鍵,如果不這么做的話,那么每次調用Objective-C方法之前,都需要為調用objC_mSgSend函數準備“棧幀”,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”。此外,若是不優化,還會過早地發生“棧溢出” (stack overflow)現象。

這樣也就理解,為何在調試的時候,棧“回溯”(backtrace)信息中總是出現objC_mSgSend。

要點:

  1. 消息由接收者、選擇子及參數構成。給某對象潑送消息"(invoke a message:也是“調用”的意思,此處為了與“call”相區隔,將其臨時譯為“發送”,也可理解為“激發”、 “觸發)”也就相當于在該對象上“調用方法”(call a method)。
  2. 發給某對象的全部消息都要由“動態消息派發系統”(dynamic message dispatch system) 來處理,該系統會査出對應的方法,并執行其代碼。

第12條:理解消息轉發機制

上面講解了對象的消息傳遞機制,本節講解對象在收到無法解讀的消息之后會發生什么情況。

若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,在編譯期向類發送了其無法解讀的消息并不會報錯,因為在運行期可以繼續向類中添加方法,所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現。當對象接收到無法解讀的消息后,就會啟動“消息轉發"(message forwarding)機制,程序員可經由此過程告沂對象應該如何處理未知消息。

你可能早就遇到過經由消息轉發流程所處理的消息了,只是未加留意。如果在控制臺中看到下面這種提示信息,那就說明你曾向某個對象發送過一條其無法解讀的消息,從而啟動了消息轉發機制,并將此消息轉發給了NSObject的默認實現。

- [_NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception
'NSInvalidArgumentException',reason: '-[_ NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87?

上面這段異常信息是由NSObject的doesNotRecognizeSelector:方法所拋出的,此異常表明:消息接收者的類型是__NSCFNumber,而該接收者無法理解名為lowercaseString的選擇子。本例所列舉的這種情況并不奇怪,因為NSNumber類里本來就沒有名為lowercaseString的方法。控制臺中看到的那__NSFCNumber是為了實現“無縫橋接"(toll-free bridging,后續將會詳解此技術)而使用的內部類(internal class),配置NSNumber對象時也會一并創建此對象。在本例中,消息轉發過程以應用程序崩潰而告終,不過,開發者在編寫自己的類時,可于轉發過程中設置掛鉤,用以執行預定的邏輯,而不使應用程序崩潰。

消息轉發分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子"(unknown selector),這叫做“動態方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉發機制”(ftill forwarding mechanism)。如果運行期系統已經把第一階段執行完了,那么接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。此時,運行期系統會請求接收者以其他手段來處理與消息相關的方法調用。這又細分為兩小步。首先,請接收者看看有沒有其他對象能處理這條消息。若有,則運行期系統會把消息轉給那個對象,于是消息轉發過程結束,一切如常。若沒有“備援的接收者”(replacement receiver),則啟動完整的消息轉發機制,運行期系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。

動態方法解析

對象在收到無法解讀的消息后,首先將調用其所屬類的下列類方法:

+ (BOOL)resolvelnstanceMethod:(SEL)selector

該方法的參數就是那個未知的選擇子,其返回值為Boolean類型,表示這個類是否能新增一個實例方法用以處理此選擇子。在繼續往下執行轉發機制之前,本類有機會新增一個處理此選擇子的方法。假如尚未實現的方法不是實例方法而是類方法,那么運行期系統就會調用另外一個方法,該方法與resolvelnstanceMethod: 類似,叫做resolveClassMethod:

使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就可以了。此方案常用來實現@dynamic屬性,因為實現這些屬性所需的存取方法在編譯期就能確定。

備援接收者

當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。與該步驟對應的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法參數代表未知的選擇子,若當前接收者能找到備援對象,則將其返回,若找不到, 就返回nil。通過此方案,我們可以用“組合”(composition)來模擬出“多重繼承”(multiple inheritance)的某些特性。在一個對象內部,可能還有一系列其他對象,該對象可經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息似的。

請注意,我們無法操作經由這一步所轉發的消息。若是想在發送給備援接收者之前先修改消息內容,那就得通過完整的消息轉發機制來做了。

完整的消息轉發

如果轉發算法已經來到這一步的話,那么唯一能做的就是啟用完整的消息轉發機制 了。首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封于其中。 此對象包含選擇子、目標(target)及參數。在觸發NSInvocation對象時,“消息派發系統” (message-dispatch system)將親自出馬,把消息指派給目標對象。

此步驟會調用下列方法來轉發消息:

- (void)forwardlnvocation:(NSInvocation *)invocation

這個方法可以實現得很簡單:只需改變調用目標,使消息在新目標上得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人采用這么簡單的實現方式。比較有用的實現方式為:在觸發消息前,先以某種方式改變消息內容,比如追加另外一個參數,或是改換選擇子,等等。實現此方法時,若發現某調用操作不應由本類處理,則需調用超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如果最后調用了 NSObject類的方法,那么該方法還會繼而調用doesNotRecognizeSelector:以拋出異常, 此異常表明選擇子最終未能得到處理。

消息轉發全流程

消息轉發機制處理消息的各個步驟.png

接收者在每一步中均有機會處理消息。步驟越往后,處理消息的代價就越大。最好能在第一步就處理完,這樣的話,運行期系統就可以將此方法緩存起來了。如果這個類的實例稍后還收到同名選擇子,那么根本無須啟動消息轉發流程。

以完整的例子演示動態方法解析

為了說明消息轉發機制的意義,下面示范如何以動態方法解析來實現@dynamic屬性。將屬性聲明為@dynamic,這樣的話,編譯器就不會為其自動生成實例變量及存取方法了

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject 
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingstore;
@end

@implementation EOCAutoDictionary
@dynamic date,opaqueObject;

- (id)init {
  if ([self == [super init]) {
    _backingStore = [NSMutableDictionary new];
  }
  return self;
}

+ (BOOL)resolvelnstanceMethod:(SEL)selector {
  NSString *selectorstring = NSStringFromSelector(selector); 
  if ([selectorstring hasPrefix: @"set"]) { 
    class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");
  } else {
    class_addMethod(self,selector,(IMP)autoDictionarySetter,"@@:");
  }
  return YES;
}

//getter函數可以用下列代碼實現:
id autoDictionaryGetter(id self, SEL _cmd) {
  //Get the backing store from the object
  EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
  NSMutableDictionary *backingStore = typedSelf.backingStore;
  //Thekey is simply the selector name 
  NSString *key = NSStringFromSelector(_cmd);
  // Return the value
  return [backingStore objectForKey:key];
}

//setter函數則可以這么寫:
void autoDictionarySetter(id self, SEL _cmd, id value) {
  //Getthe backing store from the object
  EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
  NSMutableDictionary *backingStore = typedSelf.backingStore;
  //The selector will be for example, "setOpaqueObject: ". We need to remove the "set",and lowercase the first letter of the remainder.
  NSString *selectorstring = NSStringFromSelector(_cmd);
  NSMutablestring *key = [selectorstring mutableCopy];
  // Remove the ':' at the end 
  [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
  // Remove the 'set' prefix
  [key deleteCharactersInRange:NSMakeRange(0,3)];
  // Lowercase the first character
  NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercasestring];
  [key replaceCharactersInRange:NSMakeRange(0,1) withString:lowercaseFirstChar];
  if (value) {
    [backingStore setObject:value forKey:key];
  } else {
      [backingStore removeObjectForKey:key];
  }
}
@end

當開發者首次在EOCAutoDictionary實例上訪問某個屬性時,運行期系統還找不到對應的選擇子,因為所需的選擇子既沒有直接實現,也沒有合成出來。現在假設要寫入opaqueObject屬性,那么系統就會以setOpaqueObject:為選擇子來調用上面這個方法。 同理,在讀取該屬性時,系統也會調用上述方法,只不過傳入的選擇子是opaqueObject

resolvelnslanceMethod方法會判斷選擇子的前綴是否為set,以此分辨其是set選擇子還是get選擇子。在這兩種情況下,都要向類中新增一個處理該選擇子所用的方法,這兩個方法分別以autoDictionarySetterautoDictionaryGetter函數指針的形式出現。此時就用到class_addMethod方法,它可以向類中動態地添加方法,用以處理給定的選擇子。第三個參數為函數指針,指向待添加的方法。而最后一個參數則表示待添加方法的“類型編碼”(type encoding)。在本例中,編碼開頭的字符表示方法的返回值類型,后續字符則表示其所接受的各個參數。

EOCAutoDictionary的用法很簡單:

EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSincel970:475372800];
NSLog(@"dict.date = %@",dict.date);
// Output: diet.date = 1985-01-24 00:00:00 +0000

其他屬性的訪問方式與date類似,要想添加新屬性,只需來定義,并將其聲明為@dynamic即可。在iOS的CoreAnimation框架中,CALayer類就用了與本例相似的實現方式,這使得CALayer成為兼容于“鍵值編碼的”(key-value-coding-compliant:除了使用存取方法和“點語法”之外,還可以用字符串做鍵,通過valueForKey:setValue:forKey:這種形式來訪問屬性) 容器類, 也就等于說,能夠向里面隨意添加屬性,然后以鍵值對的形式來訪問。于是,開發者就可以向其中新增自定義的屬性了,這些屬性值的存儲工作由基類直接負責,我們只需在CALayer 的子類中定義新屬性即可。

要點:

  1. 若對象無法響應某個選擇子,則進入消息轉發流程。
  2. 通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
  3. 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理。
  4. 經過上述兩步之后,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發機制。

第13條:用“方法調配技術”調試“黑盒方法”

上面講了:Objective-C對象收到消息之后,方法需要在運行期才能解析出來。那么與給定的選擇子名稱相對應的方法也可以在運行期改變。若能善用此特性,則可發揮出巨大優勢,因為我們既不需要源代碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣一來,新功能將在本類的所有實例中生效,而不是僅限于覆寫了相關方法的那些子類實例。此方案經常稱為 “方法調配”(method swizzling) 。

類的方法列表會把選擇子的名稱映射到相關的方法實現之上,使得“動態消息派發系統” 能夠據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做IMP, 其原型如下:

id (*IMP) (id, SEL,…)

NSString 類可以響應lowercaseString、uppercaseString、capitalizedString等選擇子。這張映射表中的每個選擇子都映射到了不同的IMP之上。


NSString類的選擇子映射表.png

Objective-C運行期系統提供的幾個方法都能夠用來操作這張表。開發者可以向其中新增選擇子,也可以改變某選擇子所對應的方法實現,還可以交換兩個選擇子所映射到的指針。 經過幾次操作之后,類的方法表就會變成下圖這個樣子。


經過數字操作之后的NSString選擇子映射表.png

在新的映射表中,多了一個名為newSelector的選擇子,capitalizedString的實現也變了, 而lowercaseString與uppercaseString的實現則互換了。上述修改均無須編寫子類,只要修改了“方法表”的布局,就會反映到程序中所有的NSString實例之上。

在實際應用中,直接交換兩個方法實現的意義并不大。我們一般使用該手段來為既有的方法實現增添新功能。 比方說,想要在調用lowercaseString時記錄某些信息,這時就可以通過交換方法實現來達成此目標。我們新編寫一個方法,在此方法中實現所需的附加功能,并調用原有實現。

// .h 新方法可以添加至NSString的一個“分類”(category)中:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end

//.m 新方法的實現代碼可以這樣寫:
@impleinentation NSString (EOCMyAdditions)
+ (void)load {
  //方法實現則可通過下列函數獲得:
  Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)〉;
  Method swappedMethod = class_getInstanceMethod([NSString class],@selector(eoc_myLowercaseString)); 
  //交換方法實現
  method_exchangeImplementations(originalMethod,swappedMethod);
}

- (NSString*)eoc_myLowercaseString {
  NSString *lowercase = [self eoc_myLowercaseString];
  NSLog (@"%@ => %@", self, lowercase);
  return lowercase;
)
@end

從現在開始,如果在NSString實例上調用lowercaseString,那么執行的將是uppercaseString的原有實現,反之亦然。
上述新方法將與原有的lowercaseString方法互換,交換之后的方法表如圖:


交互lowercaseString與eoc_myLowercaseString的方法實現.png

這段代碼看上去好像會陷入遞歸調用的死循環,不過大家要記住,此方法是準備和lowercaseString方法互換的。所以,在運行期,eoc_myLowercaseString選擇子實際上對應于原有的lowercaseString方法實現。

通過此方案,開發者可以為那些“完全不知道其具體實現的"(completely opaque, “完全不透明的”)黑盒方法增加日志記錄功能,這非常有助于程序調試。然而,此做法只在調試程序時有用。很少有人在調試程序之外的場合用上述“方法調配技術”來永久改動某個類的功能。不能僅僅因為Objective-C語言里有這個特性就一定要用它。若是濫用,反而會令代碼變得不易讀懂且難于維護。

要點:

  1. 在運行期,可以向類中新增或替換選擇子所對應的方法實現。
  2. 使用另一份實現來替換原有的方法實現,這道工序叫做“方法調配”,開發者常用此 技術向原有實現中添加新功能。
  3. 一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用。

第14條:理解“類對象”的用意

Objective-C實際上是一門極其動態的語言。第11條講解了運行期系統如何査找并調用某方法的實現代碼,第12條則講述了消息轉發的原理:如果類無法立即響應某個選擇子, 那么就會啟動消息轉發流程。然而,消息的接收者究竟是何物?是對象本身嗎?運行期系統如何知道某個對象的類型呢?對象類型并非在編譯期就綁定好了,而是要在運行期査找。而且,還有個特殊的類型叫做id,它能指代任意的Objective-C對象類型。一般情況下,應該指明消息接收者的具體類型,這樣的話,如果向其發送了無法解讀的消息,那么編譯器就會產生警告信息。而類型為id的對象則不然,編譯器假定它能響應所有消息。

如果看過第12條,你就會明白,編譯器無確定某類型對象到底能解讀多少種選擇子, 因為運行期還可向其中動態新增。然而,即便使用了動態新增技術,編譯器也覺得應該能在某個頭文件中找到方法原型的定義,據此可了解完整的“方法簽名"(method signature),并生成派發消息所需的正確代碼。

“在運行期檢視對象類型”這一操作也叫做“類型信息査詢”(introspection, “內省”),這個強大而有用的特性內置于Foundation框架的NSObject協議里,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來的對象都要遵從此協議。在程序中不要直接比較對象所屬的類,明智的做法是調用“類型信息査詢方法”,其原因筆者稍后解釋。不過在介紹類型信息査詢技術之前,我們先講一些基礎知識,看看Objective-C對象的本質是什么。

每個Objective-C對象實例都是指向某塊內存數據的指針(???)。所以在聲明變量時,類型后面要跟一個字符:

NSString *pointerVariable = @"Some string";

編過C語言程序的人都知道這是什么意思。對于沒寫過C語言的程序員來說, pointerVariable可以理解成存放內存地址的變量,而NSString自身的數據就存于那個地址中。 因此可以說,該變量“指向”(point to) NSString實例。所有Objective-C對象都是如此,若是想把對象所需的內存分配在棧上,編譯器則會報錯:

String stackVariable = @"Some string";
"error: interface type cannot be statically allocated

對于通用的對象類型id,由于其本身已經是指針了,所以我們能夠這樣寫:

id genericTypedString = @"Some string";

上面這種定義方式與用NSString *來定義相比,其語法意義相同。唯一區別在于,如果聲明時指定了具體類型,那么在該類實例上調用其所沒有的方法時,編譯器會探知此情況,并發出警告信息。

描述Objective-C對象所用的數據結構定義在運行期程序庫的頭文件里,id類型本身也在定義在這里:

typedef struct objc_object {
  Class isa;
} *id;

由此可見,每個對象結構體的首個成員是Class類的變量。該變量定義了對象所屬的類, 通常稱為isa指針。例如,剛才的例子中所用的對象“是一個”(isa) NSString,所以其“isa”指針就指向NSString。
Class對象也定義在運行期程序庫的頭文件中:

typedef struct objc_class *Class;
struct objc_class {
  Class isa;
  Class super_class;
  const char *name;
  long version;
  long info;
  long instance_size;
  struct objc_ivar_list *ivars;
  struct objc_method_list *methodLists;
  struct objc_cache *cache;
  struct objc_protocol list *protocols;
);

此結構體存放類的“元數據"(metadata),例如類的實例實現了幾個方法,具備多少個實例變量等信息。此結構體的首個變量也是isa指針,這說明Class本身亦為Objective-C對象。 結構體里還有個變量叫做superclass,它定義了本類的超類。類對象所屬的類型(也就是isa 指針所指向的類型)是另外一個類,叫做“元類"(metadass),用來表述類對象本身所具備的元數據。“類方法”就定義于此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個“類對象”,而每個“類對象”僅有一個與之相關的“元類”。

假設有個名為SomeClass的子類從NSObject中繼承而來,則其繼承體系如圖


SomeClass實例所屬的“類繼承體系”,此類繼承自NSObject,圖中也畫出了兩個對應“元類”之間的繼承關系.png

superclass指針確立了繼承關系,而isa指針描述了實例所屬的類。通過這張布局關圖即可執行“類型信息査詢”。我們可以査出對象是否能響應某個選擇子,是否遵從某項協議,并且能看出此對象位于“類繼承體系”(class hierarchy)的哪一部分。

在類繼承體系中查詢類型信息

可以用類型信息査詢方法來檢視類繼承體系。isMemberOfClass:能夠判斷出對象是否為某個特定類的實例,而isKindOfClass:則能夠判斷出對象是否為某類或其派生類的實例。例如:

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass: [NSDictionary class] ] ; //<NO 
[dict isMemberOfClass:[NSMutableDictionary class]];//< YES 
[dict isKindOfClass: [NSDictionary class]];//< YES 
[dict isKindOfClass:[NSArray class]]; //< NO

像這樣的類型信息査詢方法使用isa指針獲取對象所屬的類,然后通過superclass指針在繼承體系中游走。由于對象是動態的,所以此特性顯得極為重要。Objective-C與你可能熟悉的其他語言不同,在此語言中,必須査詢類型信息,方能完全了解對象的真實類型。

由于Objective-C使用“動態類型系統"(dynamic typing),所以用于査詢對象所屬類的類型信息査詢功能非常有用。從collection中獲取對象時,通常會査詢類型信息,這些對象不是“強類型的”(strongly typed),把它們從collection中取出來時,其類型通常是id。如果想知道具體類型,那就可以使用類型信息査詢方法。

也可以用比較類對象是否等同的辦法來做。若是如此,那就要使用==操作符,而不要使用比較Objective-C對象時常用的“isEqual:”方法(參見第8條)。原因在于,類對象是 “單例”(singleton),在應用程序范圍內,每個類的Class僅有一個實例。也就是說,另外一種可以精確判斷出對象是否為某類實例的辦法是:

id object = /* ??? */;
if ([object class] == [EOCSomeClass class]) {
  //'object' is an instance of EOCSomeClass
)

即便能這樣做,我們也應該盡景使用類型信息査詢方法,而不應該直接比較兩個類對象是否等同,因為前者可以正確處理那些使用了消息傳遞機制(參見第12條)的對象。比方說,某個對象可能會把其收到的所有選擇子都轉發給另外一個對象。這樣的對象叫做“代理” (proxy),此種對象均以NSProxy為根類。

通常情況下,如果在此種代理對象上調用class方法,那么返回的是代理對象本身(此類是NSProxy的子類),而非接受的代理的對象所屬的類。然而,若是改用isKindOfClass: 這樣的類型信息査詢方法,那么代理對象就會把這條消息轉給“接受代理的對象”(proxied object)。也就是說,這條消息的返回值與直接在接受代理的對象上面査詢其類型所得的結果相同。因此,這樣査出來的類對象與通過class方法所返回的那個類對象不同,class方法所返回的類表示發起代理的對象,而非接受代理的對象。

要點:

  1. 每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成了類的繼承體系。
  2. 如果對象類型無法在編譯期確定,那么就應該使用類型信息査詢方法來探知。
  3. 盡量使用類型信息査詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能。

第三章 接口與API設計

第15條:用前綴避免命名空間沖突

Objective-C沒有其他語言那種內置的命名空間(namespace)機制。鑒于此,我們在起名時要設法避免潛在的命名沖突,否則很容易就重名了。如果發生命名沖突(naming dash), 那么應用程序的鏈接過程就會出錯,因為其中出現了重復符號:

duplicate symbol _OBJC_METACLASS_$_EOCTheClass in: build/something.o build/something_else.o
duplicate symbol _OBJC_CLASS_$—EOCTheClass in: build/something.o build/something_else.o

避免此問題的唯一辦法就是變相實現命名空間:為所有名稱都加上適當前綴。所選前綴可以是與公司、應用程序或二者皆有關聯之名。Apple宣稱其保留使用所有“兩字母前綴” (two-letter prefix)的權利,所以你自己選用的前綴應該是三個字母的。

不僅是類名,還有以下:

  1. 那么一定要給“分類” (category)及“分類”中的方法加上前綴(第25條解釋了這么做的原因)。
  2. 類的實現文件中所用的純C函數及全局變量,這個問題必須要注意。因為在編譯好的目標文件中,這些名稱是要算作“頂級符號”(top-level symbol)的。

這么做的好處:若此符號出現在棧回溯信息中,則很容易就能判明問題源自哪塊代碼。

要點:

  1. 選擇與你的公司、應用程序或二者皆有關聯之名稱作為類名的前綴,并在所有代碼中均使用這一前綴。
  2. 若自己所開發的程序庫中用到了第三方庫,則應為其中的名稱加上前綴。

第16條: 提供“全能初始化方法”

為對象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer) 。令其他初始化方法都來調用它。只有在全能初始化方法中,才會存儲內部數據。這樣的話,當底層數據存儲機制改變時,只需修改此方法的代碼就好,無須改動其他初始化方法。
每個子類的全能初始化方法都應該調用其超類的對應方法,并逐層向上。

要點:

  1. 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應調用此方法。
  2. 若全能初始化方法與超類不同,則需覆寫超類中的對應方法。
  3. 如果超類的初始化方法不適用于子類,那么應該覆寫這個超類方法,并在其中拋出異常。

第17條:實現description方法

調試程序時,經常需要打印并查看對象信息。構建需要打印到日志的字符串時,object對象會收到description消息,該方法所返回的描述信息將取代“格式字符串”(format string:在這里指%@)。

當自定義對象,并想輸出更為有用的信息也很簡單,只需覆寫description方法并將描述此對象的字符串返回即可。

NSObject協議中還有個方法要注意,那就是debugDescription,此方法的用意與description非常相似。二者區別在于,debugDescription方法是開發者在調試器(debugger) 中以控制臺命令(比如LLDB的po命令)打印對象時才調用的。在NSObject類的默認實現中,此方法只是直接調用了description 。

要點:

  1. 實現description方法返回一個有意義的字符串,用以描述該實例。
  2. 若想在調試時打印出更詳盡的對象描述信息,則應實現debugDescription方法。

第18條:盡量使用不可變對象

設計類的時候,應充分運用屬性來封裝數據(參見第6條)。默認情況下,屬性是“既可讀又可寫的"(read-write),這樣設計出來的類都是“可變的”(mutable)。具體到編程實踐中,則應該盡量把對外公布出來的屬性設為只讀,而且只在確有必要時才將屬性對外公布。

當然,如果該屬性是nonatomic的,那么這樣做可能會產生“競爭條件”(racecondition)。在對象內部寫入某屬性時,對象外的觀察者也許正讀取該屬性。若想避免此問題,我們可以在必要時通過“派發隊列"(dispatch queue, 參見第41條)等手段,將(包括對象內部的)所有數據存取操作都設為同步操作。

kvc和類型查詢:

  1. 現在,只能于實現代碼內部設置這些屬性值了。其實更準確地說, 在對象外部,仍然能通過“鍵值編碼”(Key-Value Coding,KVC)技術設置這些屬性值,比如說:[pointOfInterest setValue:@"abc forKey:@"identifier"];這樣做可以改動identifier屬性,因為KVC會在類里査找setIdentifier:方法,并借此修改此屬性。不過,這樣做等于違規地繞過了本類所提供的API。
  2. 有些“愛用蠻力的”(brutal)程序員甚至不通過“設置方法”,而是直接用類型信息查詢功能査出屬性所對應的實例變量在內存布局中的偏移量,以此來人為設置這個實例變量的值。這樣做比繞過本類的公共API還要不合規范

對于集合:

  1. 如果把可變對象(mutable object)放入collection之后又修改其內容,那么很容易就會破壞set的內部數據結構,使其失去固有的語義。因此,筆者建議大家盡量減少對象中的可變內容。
  2. 對象里表示各種collection的那些屬性究競應該設成可變的,還是不可變的。在這種情況下,通常應該提供一個readonly屬性供外界使用,該屬性將返回不可變的set, 而此set則是內部那個可變set的一份拷貝。

要點:

  1. 盡量創建不可變的對象。
  2. 若某屬性僅可于對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite屬性,屬性的其他特質必須保持不變。
  3. 不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection。

第19條:使用清晰而協調的命名方式

方法與變量名使用了“駝峰式大小寫命名法"(camel casing)-以小寫字母開頭,其后每個單詞首字母大寫。類名也用駝峰命名法,不過其首字母要大寫,而且前面通常還有兩三個前綴字母(參見第15條)。在編寫Objective-C代碼時,大家一般都使用這種命名方式。

給方法命名時的注意事項可總結成下面幾條規則:

  1. 如果方法的返回值是新創建的,那么方法名的首個詞應是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,因為一般認為這些方法不會創建新對象,即便有時返回內部對象的一份拷貝,我們也認為那相當于原有的對象。這些存取方法應該按照其所對應的屬性來命名。
  2. 應該把表示參數類型的名詞放在參數前面。
  3. 如果方法要在當前對象上執行操作,那么就應該包含動詞;若執行操作時還需要參數, 則應該在動詞后面加上一個或多個名詞。
  4. 不要使用str這種簡稱,應該用string這樣的全稱。
  5. Boolean屬性應加is前綴。如果某方法返回非屬性的Boolean值,那么應該根據其功能,選用has或is當前綴。
  6. 將get這個前綴留給那些借由“輸出參數”來保存返回值的方法,比如說,把返回值填充到“C語言式數組”(C-style array)里的那種方法就可以使用這個詞做前綴。

類與協議的命名

應該為類與協議的名稱加上前綴,以避免命名空間沖突(參見第15條),如果要從其他框架中繼承子類,那么務必遵循其命名慣例。比方說,要從UlView類中繼承自定義的子類, 那么類名末尾的詞必須是View。同理,若要創建自定義的委托協議,則其名稱中應該包含委托發起方的名稱,后面再跟上Delegate—詞。

要點:

  1. 起名時應遵從標準的Objective-C命名規范,這樣創建出來的接口更容易為開發者所 理解。
  2. 方法名要言簡意賅,從左至右讀起來要像個日常用語中的句子才好。
  3. 方法名里不要使用縮略后的類型名稱。
  4. 給方法起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符。

第20條:為私有方法名加前綴

具體使用何種前綴可根據個人喜好來定,其中最好包含字母P與下劃線。比如

- (void)p_add {
  //...
}

如果寫過C++或Java代碼,你可能就會問了:為什么要這樣做呢?直接把方法聲明成私有的不就好了嗎? Objective-C語言沒辦法將方法標為私有。每個對象都可以響應任意消息(參見第12條),而且可在運行期檢視某個對象所能直接響應的消息(參見第14條)。根據給定的消息査出其對應的方法,這一工作要在運行期才能完成(參見第11條),所以 Objective-C中沒有那種約束方法調用的機制用以限定誰能調用此方法、能在哪個對象上調用此方法以及何時能調用此方法。開發者會在命名慣例中體現出“私有方法”等語義。必須用心領悟Objective-C語言這種強大的動態特性。

要點:

  1. 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開。
  2. 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的

第21條:理解Objective-C錯誤模型

當前很多種編程語言都有“異常”(exception)機制,Objective-C也不例外。寫過Java 代碼的程序員應該很習慣于用異常來處理錯誤。如果你也是這么使用異常的,那現在就把它忘了吧,我們得從頭學起。

首先要注意的是,“自動引用計數”(Automatic ReferenceCounting, ARC,參見第30條)在默認情況下不是“異常安全的"(exception safe)。具體來說,這意味著:如果拋出異常,那么本應在作用域末尾釋放的對象現在卻不會自動釋放了。如果想生成“異常安全”的代碼, 可以通過設置編譯器的標志來實現,不過這將引入一些額外代碼,在不拋出異常時,也照樣要執行這部分代碼。需要打開的編譯器標志叫做-fobjc-arc-exceptions

既然異常只用于處理嚴重錯誤(fatal error,致命錯誤),那么對其他錯誤怎么辦呢?在出現“不那么嚴重的錯誤"(nonfatalerror,非致命錯誤)時,Objective-C語言所用的編程范式為: 令方法返回nil/0,或是使用NSError,以表明其中有錯誤發生。

要點:

  1. 只有發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常。
  2. 在錯誤不那么嚴重的情況下,可以指派“委托方法”(delegate method)來處理錯誤,也可以把錯誤信息放在NSError對象里,經由“輸出參數”返回給調用者。

第22條:理解NSCopying協議

使用對象時經常需要拷貝它。在Objective-C中,此操作通過copy方法完成。如果想令自己的類支持拷貝操作,那就要實現NSCopying協議,該協議只有一個方法:

- (id)copyWithZone:(NSZone *)zone

為何會出現NSZone呢?因為以前開發程序時,會據此把內存分成不同的“區”(zone), 而對象會創建在某個區里面。現在不用了,每個程序只有一個區:“默認區”(default zone)。 所以說,盡管必須實現這個方法,但是你不必擔心其中的zone參數。

copy->_friends = [_friends mutableCopy];

這次所實現的方法比原來多了一些代碼,它把本對象的_friends實例變量復制了一份, 令copy對象的_friends實例變量指向這個復制過的set。(注意:這里使用了->語法,因為 friends并非屬性,只是個在內部使用的實例變量。其實也可以聲明一個屬性來表示它,不過由于該變量不會在本類之外使用,所以那么做沒必要)

NSMutableCopying的協議。該協議與 NSCopying類似,也只定義了一個方法,然而方法名不同:

- (id)mutableCopyWithZone:(NSZone*)zone

在編寫拷貝方法時,還要決定一個問題,就是應該執行“深拷貝”(deep copy)還是“淺拷貝”(shallowcopy)。深拷貝的意思就是:在拷貝對象自身時,將其底層數據也一并復制過去。Foundation框架中的所有collection類在默認情況下都執行淺拷貝,也就是說,只拷貝容器對象本身,而不復制其中數據。這樣做的主要原因在于,容器內的對象未必都能拷貝,而且調用者也未必想在拷貝容器時一并拷貝其中的每個對象。

以NSSet為例,該類提供了下面這個初始化方法,用以執行深拷貝:


淺拷貝與深拷貝對比圖。淺拷貝之后的內容與原始內容均指向相同對象。 而深拷貝之后的內容所指的對象是原始內容中相關對象的一份拷貝.png
-(id)initWithSet:(NSArray^)array copyltems:(BOOL)copyltems

若copyltem參數設為YES,則該方法會向數組中的每個元素發送copy消息,用拷貝好的元素創建新的set,并將其返回給調用者。

要點:

  1. 若想令自己所寫的對象具有拷貝功能,需實現NSCopying協議。
  2. 如果自定義的對象分為可變版本與不可變版本,那么就要同時實現NSCopying與 NSMutableCopying 協議。
  3. 復制對象時需決定采用淺拷貝還是深拷貝,一般情況下應該盡量執行淺拷貝。

第四章 協議與分類

Objective-C語言有一項特性叫做“協議”(protocol),它與Java的“接口"(interface)類似,Objective-C不支持多重繼承,因而我們把某個類應該實現的一系列方法定義在協議里面。 協議最為常見的用途是實現委托模式(參見第23條),不過也有其他用法。
“分類”(Category)也是Objective-C的一項重要語言特性。利用分類機制,我們無須繼承子類即可直接為當前類添加方法,而在其他編程語言中,則需通過繼承子類來實現。由于Objective-C運行期系統是髙度動態的,所以才能支持這一特性,然而,其中也隱藏著一些陷阱。

第23條:通過委托與數據源協議進行對象間通信

對象之間經常需要相互通信,而通信方式有很多種。Objective-C開發者廣泛使用一種名叫“委托模式"(Delegatepattem)的編程設計模式來實現對象間的通信,該模式的主旨是: 定義一套接口,某對象若想接受另一個對象的委托,則需遵從此接口,以便成為其“委托對象”(delegate)。而這“另一個對象”則可以給其委托對象回傳一些信息。

在Objective-C中,一般通過“協議”這項語言特性來實現此模式:

  1. 委托協議名通常是在相關類名后面加上Delegate—詞,整個類名采用“駝峰法”來寫。
  2. 有了這個協議之后,類就可以用一個屬性來存放其委托對象了。
  3. 實現委托對象的辦法是聲明某個類遵從委托協議(一般都是在“class-continuation分類”里聲明的)
  4. 把協議中想實現的那些方法在類里實現出來。

注意點:

  1. 存放委托對象的屬性需定義成weak,而非strong,因為兩者之間必須為“非擁有關系” (nonowning relationship)。通常情況下,扮演delegate的那個對象也要持有本對象。
  2. 如果要在委托對象上調用可選方法,那么必須提前使用類型信息査詢方法respondsToSelector:判斷這個委托對象能否響應相關選擇子。

大家應該很容易理解此模式為何叫做“委托模式”:因為對象把應對某個行為的責任委托給另外一個類了。也可以用協議定義一套接口,令某類經由該接口獲取其所需的數據。委托模式的這一用法旨在向類提供數據,故而又稱“數據源模式”(Data Source Pattern)。在此模式中,信息從數據源(Data Source)流向類(Class);而在常規的委托模式中,信息則從類流向受委托者 (Delegate)。
下圖演示這兩條信息流:在信息源模式中,信息從數據源流向類,而在普通的委托模式中,信息則從類流向受委托者


數據源.png

比方說,用戶界面框架中的“列表視圖”(list view)對象可能會通過數據源協議來獲取要在列表中顯示的數據。除了數據源之外,列表視圖還有一個受委托者,用于處理用戶與列表的交互操作。將數據源協議與委托協議分離,能使接口更加清晰,因為這兩部分的邏輯代碼也分開了。另外,“數據源”與“受委托者”可以是兩個不同的對象。然而一般情況下, 都用同一個對象來扮演這兩種角色。

要點:

  1. 委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象。
  2. 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的事件定義成方法。
  3. 當某對象需要從另外一個對象中獲取數據時,可以使用委托模式。這種情境下,該模式亦稱“數據源協議”(data source protocal)。
  4. 若有必要,可實現含有位段的結構體,將委托對象是否能響應相關協議方法這一信息緩存至其中。

第24條:將類的實現代碼分散到便于管理的數個分類之中

類中經常容易填滿各種方法,而這些方法的代碼則全部堆在一個巨大的實現文件里。 在此情況下,可以通過Objective-C的“分類”機制,把類代碼按邏輯劃入幾個分區中。

現在,類的實現代碼按照方法分成了好幾個部分。所以說,這項語言特性當然就叫做 “分類”啦。類的基本要素(諸如屬性與初始化方法等)都聲明在“主實現"(main implementation)里。執行不同類型的操作所用的另外幾套方法則歸入各個分類中。

使用分類機制之后,依然可以把整個類都定義在一個接口文件中,并將其代碼寫在一個實現文件里。 此時可以把每個分類提取到各自的文件中去。如果想用分類中的方法,那么要記得在引入主類時一并引入分類的頭文件。以下是原因:

  1. 隨著分類數量增加,當前這份實現文件很快就膨脹得無法管理了。
  2. 便于調試:對于某個分類中的所有方法來說,分類名稱都會出現在其符號中,在調試器的回溯信息中會出現。例如,“addFriend:”方法的“符號名”(symbol name):- [EOCPerson(Friendship) addFriend:1];

要點:

  1. 使用分類機制把類的實現代碼劃分成易于管理的小塊。
  2. 將應該視為“私有”的方法歸入名叫Private的分類中,以隱藏實現細節。

第25條:總是為第三方類的分類名稱加前綴

分類機制通常用于向無源碼的既有類中新增功能。這個特性極為強大,但在使用時也很容易忽視其中可能產生的問題。這個問題在于:分類中的方法是直接添加在類里面的,它們就好比這個類中的固有方法。將分類方法加入類中這一操作是在運行期系統加載分類時完成的。運行期系統會把分類中所實現的每個方法都加入類的方法列表中。如果類中本來就有此方法,而分類又實現了一次,那么分類中的方法會覆蓋原來那一份實現代碼。實際上可能會發生很多次覆蓋,比如某個分類中的方法覆蓋了“主實現”中的相關方法,而另外一個分類中的方法又覆蓋了這個分類中的方法。多次覆蓋的結果以最后一個分類為準。(注:不是覆蓋,是根據selector找到的第一個方法為準,其后不再遍歷查找后序的同名方法)

要解決此問題,一般的做法是:以命名空間來區別各個分類的名稱與其中所定義的方法。Obiective-C中實現命名空間功能,只有一個辦法,就是給相關名稱都加上某個共用的前綴。

要點:

  1. 向第三方類中添加分類時,總應給其名稱加上你專用的前綴。
  2. 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴。

第26條:勿在分類中聲明屬性

屬性是封裝數據的方式(參見第6條)。盡管從技術上說,分類里也可以聲明屬性,但這種做法還是要盡量避免。原因在于,除了 “class-continuation分類”(參見第27條)之外,其他分類都無法向類中新增實例變量,因此,它們無法把實現屬性所需的實例變量合成出來。

有兩種辦法可以在分類里聲明屬性:

  1. 把存取方法聲明為@dynamic, 也就是說,這些方法等到運行期再提供,編譯器目前是看不見的。如果決定使用消息轉發機制(參見第12條)在運行期攔截方法調用,并提供其實現,那么或許可以采用這種做法。
  2. 關聯對象(參見第10條)能夠解決在分類中不能合成實例變量的問題。但是在內存管理問題上容易出錯。

正確做法是把所有屬性都定義在主接口里。類所封裝的全部數據都應該定義在主接口中,這里是唯一能夠定義實例變量(也就是數據)的地方。而屬性只是定義實例變量及相關存取方法所用的“語法糖”,所以也應遵循同實例變量一樣的規則。至于分類機制,則應將其理解為一種手段,目標在于擴展類的功能,而非封裝數據。

雖說如此,但有時候只讀屬性還是可以在分類中使用的。由于獲取方法并不訪問數據,而且屬性也不需要由實例變量來實現。即便在這種情況下,也最好不要用屬性。屬性所要表達的意思是:類中有數據在支持著它。屬性是用來封裝數據的。某些情況下也可以直接聲明一個方法,用以獲取數據。

要點:

  1. 把封裝數據所用的全部屬性都定義在主接口里。
  2. 在“dass-contimiation分類”之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。

第27條:使用“class-continuation分類”隱藏實現細節

類中經常會包含一些無須對外公布的方法及實例變量。其實這些內容也可以對外公布,并且寫明其為私有,但是這樣會泄漏實現細節。Objective-C動態消息系統(參見第11條)的工作方式決定了其不可能實現真正的私有方法或私有實例變量。然而,我們最好還是只把確實需要對外公布的那部分內容公開,隱藏部分細節。那么,這個特殊的“class-continuation分類”就派上用場了。

“class-continuation分類”和普通的分類不同,它必須定義在其所接觸的那個類的實現文件里。其重要之處在于,這是唯一能聲明實例變量的分類,而且此分類沒有特定的實現文件,其中的方法都應該定義在類的主實現文件里。與其他分類不同,“class-continuation分類”沒有名字。

為什么需要有這種分類呢?因為其中可以定義方法和實例變量。為什么能在其中定義方法和實例變量呢?只因有“穩固的ABI”這一機制(第6條詳解了此機制),使得我們無須知道對象大小即可使用它。由于類的使用者不一定需要知道實例變量的內存布局,所以,它們也就未必非得定義在公共接口中了。基于上述原因,我們可以像在類的實現文件里那樣,于 “ class-contimiatiori分類”中給類新增實例變量。

實例變量也可以定義在實現塊里,從語法上說,這與直接添加到“class-continuation 分類”等效,只是看個人喜好了。筆者喜歡將其添加在“dass-continuation分類”中。這些實例變量并非真的私有,因為在運行期總可以調用某些方法繞過此限制,不過,從一般意義上來說,它們還是私有的。

最后還要講一種用法:比方說,EOCPerson遵從了名為EOCSecretDelegate的協議。聲明在公共接口里。你可能會說,只需要向前聲明EOCSecretDelegate協議就可以不引入它了(或者說,不引入定義該協議的頭文件了)。用下面這行向前聲明語句來取代#import指令:

@protocol EOCSecretDelegate;

但是這樣一來,只要引入EOCPerson頭文件的地方,由于編譯器看不到協議的定義,所以無法得知其中所含的方法,于是編譯器會給出瞥告信息。

要點:

  1. 通過“class-continuation分類”向類中新增實例變量。
  2. 如果某屬性在主接口中聲明為“只讀”,而類的內部又要用設置方法修改此屬性,那 么就在“class-continuation分類”中將其擴展為“可讀寫”。
  3. 把私有方法的原型聲明在“class-continuation分類”里面。
  4. 若想使類所遵循的協議不為人所知,則可于“class-continuation分類”中聲明。

第28條:通過協議提供匿名對象

協議定義了一系列方法,遵從此協議的對象應該實現它們(如果這些方法不是可選的, 那么就必須實現)。于是,我們可以用協議把自己所寫的API之中的實現細節隱藏起來,將返回的對象設計為遵從此協議的純id類型。這樣的話,想要隱藏的類名就不會出現在API之中了。若是接口背后有多個不同的實現類,而你又不想指明具體使用哪個類,那么可以考慮用這個辦法——因為有時候這些類可能會變,有時候它們又無法容納于標準的類繼承體系中,因而不能以某個公共基類來統一表示。

此概念經常稱為“匿名對象"(anonymous object),這與其他語言中的“匿名對象”不同, 在那些語言中,該詞是指以內聯形式所創建出來的無名類,而此詞在Objective-C中則不是這個意思。第23條解釋了委托與數據源對象,其中就曾用到這種匿名對象。例如,在定義 “受委托者”(delegate)這個屬性時,可以這樣寫:

@property {nonatomic, weak) id <EOCDelegate> delegate;

由于該屬性的類型是id <EOCDelegate>,所以實際上任何類的對象都能充當這一屬性, 即便該類不繼承自NSObject也可以,只要遵循EOCDelegate協議就行。對于具備此屬性的類來說,delegate就是“匿名的”(ammymous)。如有需要,可在運行期査出此對象所屬的類型(參見第14條)。然而這樣做不太好,因為指定屬性類型時所寫的那個EOCDelegate契約已經表明此對象的具體類型無關緊要了。

NSDictionary也能實際說明這一概念。在字典中,鍵的標準內存管理語義是“設置時拷貝”,而值的語義則是“設置時保留”。因此,在可變版本的字典中,設置鍵值對所用的方法的簽名是:

- (void)setObject:(id)object forKey:(id<NSCopying>)key;

表示鍵的那個參數其類型為id<NSCopying>,作為參數值的對象,它可以是任何類型, 只要遵從NSCopying協議就好,這樣的話,就能向該對象發送拷貝消息了。這個key參數可以視為匿名對象。與delegate屬性一樣,字典也不關心key對象所屬的具 體類,而且它也決不應該依賴于此。字典對象只要能確定它可以給此實例發送拷貝消息就行了。

有時對象類型并不重要,重要的是對象有沒有實現某些方法,在此情況下,也可以用這些“匿名類型”(anonymous type)來表達這一概念。即便實現代碼總是使用固定的類,你可能還是會把它寫成遵從某協議的匿名類型,以表示類型在此處并不重要。

要點:

  1. 協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某協議的id類型,協議里規定了對象所應實現的方法。
  2. 使用匿名對象來隱藏類型名稱(或類名)。
  3. 如果具體類型不重要,重要的是對象能夠響應(定義在協議里的)特定方法,那么可使用匿名對象來表示。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容