Objective-C 2.0 with Cocoa Foundation--- 8,類方法以及私有方法

原文鏈接:http://www.cnblogs.com/yaski/


 8,類方法以及私有方法

本系列講座有着很強的前後相關性,如果你是第一次閱讀本篇文章,爲了更好的理解本章內容,筆者建議你最好從本系列講座的第1章開始閱讀,請點擊這裏 

Objective-C裏面區別於實例方法,和Java或者C++一樣,也支持類方法。類方法(Class Method) 有時被稱爲工廠方法(Factory Method)或者方便方法(Convenience method)。工廠方法的稱謂明顯和一般意義上的工廠方法不同,從本質上來說,類方法可以獨立於對象而執行,所以在其他的語言裏面類方法有的時候被稱爲靜態方法。就像@interface曾經給我們帶來的混亂一樣,現在我們就不去追究和爭論工廠方法的問題了,我們看到Objective-C的文章說工廠方法,就把它當作類方法好了。

在Objective-C裏面,最受大家歡迎的類方法應該是alloc,我們需要使用alloc來爲我們的對象分配內存。可以想象,如果沒有alloc,我們將要如何來爲我們的類分配內存!

和其他的語言類似,下面是類方法的一些規則,請大家務必記住。 

1,類方法可以調用類方法。

2,類方法不可以調用實例方法,但是類方法可以通過創建對象來訪問實例方法。

3,類方法不可以使用實例變量。類方法可以使用self,因爲self不是實例變量。

4,類方法作爲消息,可以被髮送到類或者對象裏面去(實際上,就是可以通過類或者對象調用類方法的意思)。

如果大家觀察一下Cocoa的類庫,會發現類方法被大量的應用於方便的對象創建和操作對象的,考慮到類方法的上述的特性,同學們在設計自己的類的時候,爲了謀求這種方便,可以考慮使用類方法來創建或者操作對象。筆者認爲,這個就是類方法的潛規則,在本章的範例程序裏面,筆者將要遵守這個潛規則。

在上一章我們講了一下實例變量的作用域,實例變量的作用域的方式和其他面向對象的語言沒有什麼不同。對於方法,非常遺憾的是,Objective-C並沒有爲我們提供諸如public,private和protected這樣的限定,這就意味着在Objective-C裏面,從理論上來說所有的方法都是公有的。但是,我們可以利用Objective-C的語言的特性,我們自己來實現方法的私有化。當然我們自己的私有化手段沒有得到任何的編譯器的支持,只是告訴使用者:“這是一個私有的方法,請不要使用這個方法”。所以,無論作爲類的設計者和使用者都應該清楚在Objective-C裏面的方法私有化的所有手段,這樣就在類的設計者和使用者之間達成了一種默契,這種方式明顯不是Objective-C語法所硬性規定的,所以也可以把這種手法成爲一種潛規則。

關於潛規則經常看英文文檔的同學,應該可以遇到這樣一個詞,de facto standard,也就是筆者所說的潛規則。

本章所述的方法的私有化是一種有缺陷的手段,有一定的風險而且也沒有完全實現私有化,在後面的章節裏面筆者會陸續的給出其他的實現方法私有化的方法。

另外,Objective-C裏面有一個其他不支持指針的語言沒有的一個動態特性,那就是程序在執行的時候,可以動態的替換類的手段。動態的方法替換有很多種應用,本章實現了一個類似java裏面的final函數。和final函數不同的是,如果子類重寫了這個方法,編譯器不會報錯,但是執行的時候總是執行的你的超類的方法。

類方法,方法私有化和動態方法替換將是本章的主題。

8.1,本章程序的執行結果

在本章裏面,我們將要繼續使用我們在第4章已經構築好的類Cattle和Bull。 

筆者在這裏暫時違反一下不修改已經生效的代碼規則改寫了一下Cattle和Bull類,在裏面追加了一些類方法,用於創建Cattle系列的對象。

筆者也改寫了Cattle的頭文件用來實現方法的私有化。

面向對象的程序有一個很大的特色就是動態性,但是由於某種原因我們在設計超類的時候,也許會考慮把某個方法設定成爲靜態的,這樣就有了諸如final的概念。在本章我們將要使用動態的方法替換來實現這個功能。我們將要構築一個新類,名字叫做UnknownBull,我們使用動態方法替換導致即使UnknownBull重載了Cattle類的saySomething,但是向UnknownBull發送saySomething的時候,仍然執行的是Cattle的saySomething。本章程序的執行結果請參照下圖:

 

圖8-1,本章程序的執行結果。

本章程序可以點擊這裏下載。

8.2,實現步驟

第一步,按照我們在第2章所述的方法,新建一個項目,項目的名字叫做07-InitWithAndIvarScope。如果你是第一次看本篇文章,請到這裏參看第二章的內容。

第二步,按照我們在第4章的4.2節的第二,三,四步所述的方法,把在第4章已經使用過的“Cattle.h”,“Cattle.m”,“Bull.h”還有“Bull.m”, 導入本章的項目裏面。

第三步,打開“Cattle.h”和“Cattle.m”,分別修改成爲下面的代碼並且保存:

#import <Foundation/Foundation.h>

@interface Cattle : NSObject {
    
int legsCount;
}
- (void)saySomething;
+ (id) cattleWithLegsCountVersionA:(int) count;
+ (id) cattleWithLegsCountVersionB:(int) count;
+ (id) cattleWithLegsCountVersionC:(int) count;
+ (id) cattleWithLegsCountVersionD:(int) count;
@end

 

#import "Cattle.h"
#import 
<objc/objc-class.h>

@implementation Cattle
-(void) saySomething
{
    NSLog(
@"Hello, I am a cattle, I have %d legs.", legsCount);
}
-(void) setLegsCount:(int) count
{
    legsCount 
= count;
}
+ (id) cattleWithLegsCountVersionA:(int) count
{
    id ret 
= [[Cattle alloc] init];
    
//NEVER DO LIKE BELOW
    
//legsCount = count;
    [ret setLegsCount:count];
    
return [ret autorelease];
}
+ (id) cattleWithLegsCountVersionB:(int) count
{
    id ret 
= [[[Cattle alloc] init] autorelease];
    [ret setLegsCount:count];
    
return ret;    
}
+ (id) cattleWithLegsCountVersionC:(int) count
{
    id ret 
= [[self alloc] init];
    [ret setLegsCount:count];
    
return [ret autorelease];
}
+ (id) cattleWithLegsCountVersionD:(int) count 
{
    id ret 
= [[self alloc] init];
 [ret setLegsCount:count];
    
    
if([self class== [Cattle class])
       
return [ret autorelease];

    SEL sayName 
= @selector(saySomething);
    Method unknownSubClassSaySomething 
= class_getInstanceMethod([self class], sayName);
    
//Change the subclass method is RUDE!
    Method cattleSaySomething = class_getInstanceMethod([Cattle class], sayName);
    
//method_imp is deprecated since 10.5
    unknownSubClassSaySomething->method_imp = cattleSaySomething->method_imp;

    
return [ret autorelease];
}
@end

第四步,打開“Bull.h”和“Bull.m”,分別修改成爲下面的代碼並且保存:

#import <Foundation/Foundation.h>
#import 
"Cattle.h"

@interface Bull : Cattle {
    NSString 
*skinColor;
}
- (void)saySomething;
- (NSString*) getSkinColor;
- (void) setSkinColor:(NSString *) color;
+ (id) bullWithLegsCount:(int) count bullSkinColor:(NSString*) theColor;
@end

 

#import "Bull.h"

@implementation Bull
-(void) saySomething
{
    NSLog(
@"Hello, I am a %@ bull, I have %d legs.", [self getSkinColor],legsCount);
}
-(NSString*) getSkinColor
{
    
return skinColor;
}
- (void) setSkinColor:(NSString *) color
{
    skinColor 
= color;
}
+ (id) bullWithLegsCount:(int) count bullSkinColor:(NSString*) theColor
{
    id ret 
= [self cattleWithLegsCountVersionC:count];
    [ret setSkinColor:theColor];
    
//DO NOT USE autorelease here!
    return ret;
}
@end

第五步,創建一個新類,名字叫做“UnknownBull”,然後分別打開“UnknownBull.h”和“UnknownBull.m”,分別修改成爲下面的代碼並且保存: 

#import <Foundation/Foundation.h>
#import 
"Bull.h"

@interface UnknownBull : Bull {

}
-(void)saySomething;
@end

 

#import "UnknownBull.h"

@implementation UnknownBull
-(void)saySomething
{
    NSLog(
@"Hello, I am an unknown bull.");
}
@end

第六步,打開“08-Class_Method_And_Private_Method.m” ,修改成爲下面的樣子並且保存

#import <Foundation/Foundation.h>
#import 
"Cattle.h"
#import 
"Bull.h"
#import 
"UnknownBull.h"
int main (int argc, const char * argv[]) {
    NSAutoreleasePool 
* pool = [[NSAutoreleasePool alloc] init];

    id cattle[
5];
    cattle[
0= [Cattle cattleWithLegsCountVersionA:4];
    cattle[
1= [Bull cattleWithLegsCountVersionB:4];
    cattle[
2= [Bull cattleWithLegsCountVersionC:4];
    cattle[
3= [Bull bullWithLegsCount:4 bullSkinColor:@"red"];
    cattle[
4= [UnknownBull cattleWithLegsCountVersionD:4];
    
    
for(int i = 0 ; i < 5 ; i++)
    {
        [cattle[i] saySomething];
    }
    [pool drain];
    
return 0;
}

第七步,選擇屏幕上方菜單裏面的“Run”,然後選擇“Console”,打開了Console對話框之後,選擇對話框上部中央的“Build and Go”,如果不出什麼意外的話,那麼應該出現入圖8-1所示的結果。如果出現了什麼意外導致錯誤的話,那麼請仔細檢查一下你的代碼。如果經過仔細檢查發現 還是不能執行的話,可以到這裏下載筆者爲同學們準備的代碼。 如果筆者的代碼還是不能執行的話,請告知筆者。

8.2,方法的私有化

 

在講述方法私有化之前,我們首先要提到一個Objective-C裏面的一個概念,動態類型和靜態類型。

所謂的動態類型,就是使用id來定義一個對象,比如說

id cattle = [[Cattle alloc] init];

所謂的靜態類型,就是使用已知變量的的類型來定義對象,比如說

 

Cattle cattle = [[Cattle alloc] init];

動態類型和靜態類型各有好處,動態類型實現了多態性,使用靜態類型的時候編譯器會爲你檢查一下也許會出現危險的地方,比如說向一個靜態類型的對象發送一個它沒有定義的消息等等。 

好的,我們現在打開“cattle.h”,大家可以發現,和以前的版本相比,我們的“cattle.h”少了一個方法的定義,那就是-(void) setLegsCount:(int) count;。筆者在本章的範例程序裏面實現私有方法的手段比較簡單,直接把-(void) setLegsCount:(int) count從“cattle.h”給刪除掉了。

大家打開““cattle.m”,可以看到裏面-(void) setLegsCount:(int) count是有實現部分的。實現部分和過去的版本沒有任何區別的。

我們本章裏面講述的實現方法私有化的手段,就是從頭文件當中不寫方法的聲明。這樣做會導致如下幾個現象

1,在類的實現文件.m裏面,你可以向平常一樣使用[self setLegsCount:4] 來發送消息,但是確省設定的編譯器會很不禮貌的給你一個警告。

2,你可以向Cattle以及從Cattle繼承的類的靜態對象發送setLegsCount:4的消息,但是同樣,確省設定的編譯器會很不禮貌的給你一個警告。

3,你可以向Cattle以及從Cattle繼承的類的動態對象發送setLegsCount:4的消息,編譯器不會向你發送任何警告的。

說到這裏,同學們也許會覺得這一節的方法私有化有一點奇怪,因爲在上面的第二條裏面,不能阻止對對象的私有方法進行調用。令我們更爲惱火的是,居然在我們自己的類的實現文件裏面需要調用的時候產生諸如第一條的警告!

讓我們冷靜一下。

我們說,在面向對象的程序裏面,一般而言類的使用者只關心接口,不關心實現的。當我們類的實現部分的某個方法,在頭文件裏面沒有定義的話,那麼由於我們的類的使用者只是看頭文件,所以他不應該是用我們定義的所謂的私有方法的。這一點,對於其他的語言來說也是一樣的,其他的語言的私有方法和變量,如果我們把它們改爲public,或者我們不修改頭文件,使用指針也可以強行的訪問到私有的變量和方法的,從這個角度上來說,私有化的方法和變量也只不過是一個擺設而已,沒有人可以阻止我們去訪問他們,探求埋藏在裏面的奧祕。所謂的私有化只不過是一個潛規則而已,在正常的時候,我們大家都會遵守這個潛規則的。但是被逼無奈走投無路的時候我們也許會除了訪問私有的東西無可選擇。但是也不能過分,我們顯然不可以把訪問私有變量和函數當作一種樂趣。

說到這裏,我想大家應該可以理解這種私有化方法的定義了。它只不過是一種信號,告訴類的使用者,“這是一個私有的函數,請不要使用它,否則後果自負” 。我們在看到別人的代碼的時候看到了這種寫法的時候,或者別人看到我們的代碼的時候,大家都需要做到相互理解對方的隱藏私有部分的意圖。還是還是這句話,在大多數時候,請不要破壞潛規則。

8.3, 類方法

我們現在轉到本章最重要的主題,類方法。我們將要首先關注一下類方法的聲明,現在請同學們打開"Cattle.h"文件,可以發現下面的代碼:

1 + (id) cattleWithLegsCountVersionA:(int) count;
2 + (id) cattleWithLegsCountVersionB:(int) count;
3 + (id) cattleWithLegsCountVersionC:(int) count;
4 + (id) cattleWithLegsCountVersionD:(int) count;

類方法和實例方法在聲明上的唯一的區別就是,以加號+爲開始,其餘的部分是完全一致的。 筆者在這裏定義了4個不同版本的類方法,從功能上來說都是用來返回Cattle類或者其子類的對象的,其中cattleWithLegsCountVersionA到C是我們這一節講解的重點。

讓我們首先打開“Cattle.m” ,關注一下下面的代碼:

 1 + (id) cattleWithLegsCountVersionA:(int) count
 2 {
 3     id ret = [[Cattle alloc] init];
 4     //NEVER DO LIKE BELOW
 5     //legsCount = count;
 6     [ret setLegsCount:count];
 7     return [ret autorelease];
 8 }
 9 + (id) cattleWithLegsCountVersionB:(int) count
10 {
11     id ret = [[[Cattle alloc] init] autorelease];
12     [ret setLegsCount:count];
13     return ret;    
14 }

 

 

我們需要使用類方法創建對象,所以在第3行,我們使用了我們比較熟悉的對象的創建的方法創建了一個對象。大家注意一下第5行,由於類方法是和對象是脫離的所以我們是無法在類方法裏面使用實例變量的。第6行,由於我們創建了對象ret,所以我們可以向ret發送setLegsCount:這個消息,我們通過這個消息,設定了Cattle的legsCount實例變量。在第7行,我們遇到了一個新的朋友,autorelease。我們在類方法裏面創建了一個對象,當我們返回了這個對象之後,類方法也隨之結束,類方法結束就意味着在我們寫的類方法裏面,我們失去了對這個對象的參照,也就永遠無法在類方法裏面控制這個對象了。在Objective-C裏面有一個規則,就是誰創建的對象,那麼誰就有負責管理這個對象的責任,類方法結束之後,除非和類的使用者商量好了讓類的使用者釋放內存,否則我們無法直接的控制這個過程。

記憶力好的同學應該可以回憶起來,筆者曾經在第二章提到過一種延遲釋放內存的技術,這個就是autorelease。關於autorelease以及其他的內存管理方法,我們將在下一章放到一起講解。到這裏大家記住,使用類方法的潛規則是你要使用類方法操作對象,當你需要使用類方法創建一個對象的時候,那麼請在類方法裏面加上autorelease。

我們來看看cattleWithLegsCountVersionB的實現部分的代碼,和cattleWithLegsCountVersionA唯一區別就是我們在創建的時候就直接的加上了autorelease。這樣符合創建對象的時候“一口氣”的把所有需要的方法都寫到一起的習慣,採取什麼方式取決於個人喜好。

我們再打開“08-Class_Method_And_Private_Method.m”,參看下面的代碼

1     cattle[0= [Cattle cattleWithLegsCountVersionA:4];
2     cattle[1= [Bull cattleWithLegsCountVersionB:4];

 

我們在回頭看看本章程序的執行結果,心細的同學也許發現了一個很嚴重的問題,我們在第2行代碼裏面想要返回一個Bull的對象,但是輸出的時候卻變成了Cattle,原因就是我們在cattleWithLegsCountVersionB裏面創建對象的時候,使用了id ret = [[[Cattle alloc] init] autorelease]。由於Bull裏面沒有重寫cattleWithLegsCountVersionB,所以除非我們重寫cattleWithLegsCountVersionB否則我們向Bull發送cattleWithLegsCountVersionB這個類方法的時候,只能得到一個Cattle的對象。我們可以要求我們的子類的設計者在他們的子類當中重寫cattleWithLegsCountVersionB,但是這樣明顯非常笨拙,失去了動態的特性。我們當然有辦法解決這個問題,現在請大家回到“Cattle.m”,參照下列代碼:

1 + (id) cattleWithLegsCountVersionC:(int) count
2 {
3     id ret = [[self alloc] init];
4     [ret setLegsCount:count];
5     return [ret autorelease];
6 }

我們的解決方案就在第3行,我們不是用靜態的Cattle,而是使用self。說到這裏也許大家有些糊塗了,在其他的語言當中和self比較類似的是this指針,但是在Objective-C裏面self和this有些不大一樣,在類函數裏面的self實際上就是這個類本身。大家可以打開debugger觀察一下,self的地址就是Bull的Class的地址。所以程序執行到上面的代碼的第3行的時候,實際上就等同於id ret = [[[Bull class] alloc] init];

我們可以在類方法裏面使用self,我們可否通過使用self->legsCount來訪問實例變量呢?答案是不可以,因爲在這個時候對象沒有被創建也就是說,沒有爲legsCount分配內存,所以無法訪問legsCount。

由於Bull類在程序被調入內存的時候就已經初始化好了,Bull類裏面的實例函數應該被放到了代碼段,所以從理論上來說,我們可以通過使用[self setLegsCount:count]來調用實例方法的,但是不幸的是Objective-C沒有允許我們這樣做,我們在類方法中使用self來作爲消息的接收者的時候,消息總是被翻譯成爲類方法,如果發送實例方法的消息的話,會在執行的時候找不到從而產生異常。這樣做是有一定的道理的,因爲一般而言,實例方法裏面難免要使用實例變量,在類方法當中允許使用實例方法,實際上也就允許使用實例變量。

關於self大家需要記住下面的規則:

1,實例方法裏面的self,是對象的首地址。

2,類方法裏面的self,是Class.

儘管在同一個類裏面的使用self,但是self卻有着不同的解讀。在類方法裏面的self,可以翻譯成class self;在實例方法裏面的self,應該被翻譯成爲object self。在類方法裏面的self和實例方法裏面的self有着本質上的不同,儘管他們的名字都叫self。

請同學們再次回到圖8-1,可以發現通過使用神奇的self,我們動態的創建了Bull類的對象。但是等一下,我們的程序並不完美,因爲Bull類的skinColor並沒有得到初始化,所以導致了null的出現。我們在設計Cattle類也就是Bull的超類的時候,明顯我們無法預測到Bull類的特徵。消除這種問題,我們可以在得到了Bull對象之後使用setSkinColor:來設定顏色,當然我們也可以直接寫一個Bull類的方法,來封裝這個操作,請同學們打開“Bull.h”:

+ (id) bullWithLegsCount:(int) count bullSkinColor:(NSString*) theColor;

我們追加了一個類方法, bullWithLegsCount:bullSkinColor:用於創建Bull對象,請同學們打開“Bull.m”:

1 + (id) bullWithLegsCount:(int) count bullSkinColor:(NSString*) theColor
2 {
3     id ret = [self cattleWithLegsCountVersionC:count];
4     [ret setSkinColor:theColor];
5     //DO NOT USE autorelease here!
6     return ret;
7 }

上面這一段代碼相信大家都可以看明白,筆者就不在這裏贅述了。但是筆者需要強調一點,在這裏我們不需要調用autorelease的,因爲我們沒有在這裏創建任何對象。 

經過了這個改造,通過在“08-Class_Method_And_Private_Method.m”裏面我們使用

cattle[3= [Bull bullWithLegsCount:4 bullSkinColor:@"red"];

 

使得我們的代碼終於正常了,請參照圖8-1的第4行輸出。

8.4,使用動態方法替換實現final功能

首先請同學們打開“Cattle.m”,參照下面的代碼片斷:

+ (id) cattleWithLegsCountVersionD:(int) count 
{
    id ret 
= [[self alloc] init];
    [ret setLegsCount:count];
    
    
if([self class== [Cattle class])
       
return [ret autorelease];

    SEL sayName 
= @selector(saySomething);
    Method unknownSubClassSaySomething 
= class_getInstanceMethod([self class], sayName);
    
//Change the subclass method is RUDE!
    Method cattleSaySomething = class_getInstanceMethod([Cattle class], sayName);
    
//method_imp is deprecated since 10.5
    unknownSubClassSaySomething->method_imp = cattleSaySomething->method_imp;

    
return [ret autorelease];
}
@end

在cattleWithLegsCountVersionD裏面,我們將要通過使用動態的方法替換技術來實現final方法。
第3,4行代碼,是用於創建Cattle或者從Cattle類繼承的對象,並且設定實例變量legsCount。
第6,7行代碼,是用來判斷調用這個類方法的self是不是cattle,如果是cattle的話,那麼就直接返回,因爲我們要在這個方法裏面把子 類的saySomething替換成爲Cattle的saySomething,如果類是Cattle的話,那麼很明顯,我們不需要做什麼事情的。
第9行代碼是老朋友了,我們需要得到方法的SEL。

第10行和第12行,我們需要通過Objective-C的一個底層函數,class_getInstanceMethod來取得方法的數據結構 Method。讓我們把鼠標移動到Method關鍵字上面,點擊鼠標右鍵盤,選擇“Jump to definition”,我們可以看到在文件“objc-class.h”裏面的Method的定義。Method實際上是類方法在Class裏面的數據結構,系統會使用Method的信息來構築Class的信息。在Method類型的聲明裏面,我們看到了下面的代碼

typedef struct objc_method *Method;

struct objc_method {
  SEL method_name;
  
char *method_types;
  IMP method_imp;
};

其中SEL和IMP我們已經很熟悉了,method_types是方法的類型信息,Objective-C使用一些預定義的宏來表示方法的類型,然後把這些信息放到method_types裏面。

需要強調的是,蘋果在10.5之後就降級了很多Objective-C 底層的函數,並且在64位的應用當中使得這些函數失效,筆者對剝奪了衆多程序員的自由而感到遺憾。

 

第14行的代碼,我們把子類的函數指針的地址替換成爲Cattle類的saySomething,這樣無論子類是否重寫saySomething, 執行的時候由於runtime需要找到方法的入口地址,但是這個地址總是被我們替換爲Cattle的saySomething,所以子類通過 cattleWithLegsCountVersionD取得對象之後,總是調用的Cattle的saySomething,也就實現了final。當 然,這種方法有些粗魯,我們強行的不顧後果的替換了子類的重寫。

重要本節提到的final的實現方法,沒有任何蘋果官方的文檔建議這樣做,純屬筆者自創僅供大家參考,如果使用風險自擔。

 替換的結果,就是雖然我們在“08-Class_Method_And_Private_Method.m”裏面的cattle[4]l裏面使用UnknownBull是圖返回UnknownBull對象,我們也確實得到了UnknownBull對象,但是不同的是,我們在cattleWithLegsCountVersionD裏面狸貓換太子,把UnknownBull的saySomething變成了Cattle的saySomething。

讓我們回到圖8-1,我們發現最後一行的輸出爲Cattle的saySomething。

關於final的實現方式,我們當然可以使用一個文明的方法來告知子類的使用者,我們不想讓某個方法被重寫。我們只需要定義一個宏

#define FINAL

類的使用者看到這個FINAL之後,筆者相信在絕大多數時候,他會很配合你不會重寫帶FINAL定義的方法的。

8.5,本章總結

我們在本章裏面講述了方法私有化,類方法的定義和使用,動態方法替換等技術手段,也給大家強調和澄清了self的概念。
更重要的是,筆者向大家介紹了一些潛規則,希望大家可以遵守。
非常感謝大家這些天對我的鼓勵以及支持! 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章