文章目錄
4,繼承
本系列講座有著很強的前後相關性,如果你是第一次閱讀本篇文章,為了更好的理解本章內容,筆者建議你最好從本系列講座的第1章開始閱讀,請點擊這裡。
上一章筆者介紹了一下在Objective-C裡面的類的基本構造和定義以及聲明的方法。我們知道在物件導向的程式裡面,有一個很重要的需求就是代碼的重複使用,代碼的重複使用的重要方法之一就是繼承。我們在這一章裡面,將要仔細的分析一下繼承的概念以及使用的方法。有過其他物件導向語言的同學,對這一章的內容應該不會感到陌生。
4.1,本章的程式的執行結果
在本章裡面,我們將要重複使用第3章的部分代碼。我們在第3章構築了一個叫做Cattle的類,我們在這一章裡面需要使用Cattle類,然後基於Cattle類,我們需要構築一個子類,叫做Bull類。 Bull類裡面,我們追加了一個執行個體變數,名字叫做skinColor,我們也將要追加2個執行個體方法,分別getSkinColor還有setSkinColor。我們然後需要更改一下我們的main函數,然後在main函數裡面讓我們的Bull做一下重要講話。第4章程式的執行結果4-1所示:
圖4-1,本章程式的執行結果
4.2,實現步驟
第一步,按照我們在第二章所述的方法,建立一個項目,項目的名字叫做04-Hello Inheritance。如果你是第一次看本篇文章,請到這裡參看第二章的內容。
第二步,把滑鼠移動到項目瀏覽器上面的“Source”上面,然後在彈出的菜單上面選擇“Add”,然後在子功能表裡面選擇“Exsiting Files” ,4-2所示
圖4-2,向項目追加檔案
第三步,在檔案選擇菜單裡面,選擇第3章的專案檔夾“03-Hello Class”,開啟這個檔案夾之後,用滑鼠和蘋果電腦的COMMAND鍵,選澤檔案“Cattle.h”和“Cattle.m”,然後按下“Add”按鈕,4-3所示。如果你沒有下載第3章的代碼,請點擊這裡下載。
圖4-3,選擇檔案
第四步,在追加檔案的選項對話方塊裡面,讓“Copy items into destination group's folder(if needed)” 的單選框變為被選擇的狀態。這樣就保證了我們在第三步裡面選擇的檔案被拷貝到了本章的項目裡面,可以避免我們不小心更改“Cattle.h”和“Cattle.m”對已經生效的第3章程式產生影響,雖然我們在本章裡面不更改這2個代碼。
第五步,把滑鼠移動到項目瀏覽器上面的“Source”上面,然後在彈出的菜單上面選擇“Add”,然後在子功能表裡面選擇“New Files”,然後在建立檔案對話方塊的左側選擇“Cocoa Touch Classes”,然後在右側視窗選擇“NSObject subclass”,選擇“Next”,在“New File”對話方塊裡面的“File Name”欄內輸入“Bull.m”。在這裡筆者沒有給出圖例,在這裡建立檔案的步驟和第3章的第二步到第四步相同,只是檔案名稱字不一樣。第一次看到本篇文章的同學可以參照第3章。
第六步,開啟Bull.h做出如下修改,並且儲存。
#import <Foundation/Foundation.h>
#import "Cattle.h"
@interface Bull : Cattle {
NSString *skinColor;
}
- (void)saySomething;
- (NSString*) getSkinColor;
- (void) setSkinColor:(NSString *) color;
@end
第七步,開啟Bull.m做出如下修改,並且儲存
#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;
}
@end
第八步,開啟04-Hello Inheritance.m檔案,做出如下修改,並且儲存
#import <Foundation/Foundation.h>
#import "Cattle.h"
#import "Bull.h"
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id cattle = [Cattle new];
[cattle setLegsCount:4];
[cattle saySomething];
id redBull = [Bull new];
[redBull setLegsCount:4];
[redBull setSkinColor:@"red"];
[redBull saySomething];
Bull *blackBull = [Bull new];
[blackBull setLegsCount:4];
[blackBull setSkinColor:@"black"];
[blackBull saySomething];
[pool drain];
return 0;
}
第九步,選擇螢幕上方菜單裡面的“Run”,然後選擇“Console”,開啟了Console對話方塊之後,選擇對話方塊上部中央的“Build and Go”,如果不出什麼意外的話,那麼應該出現入圖4-1所示的結果。如果出現了什麼意外導致錯誤的話,那麼請仔細檢查一下你的代碼。如果經過仔細檢查發現 還是不能執行的話,可以到這裡下載筆者為同學們準備的代碼。 如果筆者的代碼還是不能執行的話,請告知筆者。
4.3,子類Subclass和超類Superclass
讓我們首先回憶一下第3章的Cattle.h,在Cattle.h裡面我們有如下的代碼片斷:
@interface Cattle : NSObject {
這段代碼是在告訴編譯器,我們的Cattle是繼承的NSObject。在這段代碼當中,NSObject是超類,Cattle是子類。通過這樣寫,我們曾經免費的得到了NSObject裡面的一個方法叫做new。
id cattle = [Cattle new];
在物件導向的程式設計當中,如果在子類當中繼承了超類的話,那麼超類當中已經生效的部分代碼在子類當中仍然是有效,這樣就大大的提高了代碼的效率。基於超類我們可以把我們需要追加的一些功能放到子類裡面去,在本章裡面,我們決定基於Cattle類,重建一個子類Bull:
1 #import <Foundation/Foundation.h>
2 #import "Cattle.h"
3
4 @interface Bull : Cattle {
5 NSString *skinColor;
6 }
7 - (void)saySomething;
8 - (NSString*) getSkinColor;
9 - (void) setSkinColor:(NSString *) color;
10 @end
上段代碼裡面的第2行,是通知編譯器,我們這個類的聲明部分需要Cattle.h檔案。這個檔案我們已經很熟悉了,是我們在第3章曾經構築過的,在本章裡面,我們不會改變裡面的任何內容。
第4行,就是在通知編譯器,我們需要聲明一個類名字叫做Bull,從Cattle裡面繼承過來。
第5行,我們追加了一個執行個體變數skinColor,用來儲存Bull的顏色。
第7行,我們重載了在Cattle類裡面已經有的(void)saySomething執行個體方法。重載(void)saySomething方法的主要原因是,我們認為Bull說的話應該和Cattle有所區別。
第8行到第9行,我們為Bull類聲明了兩個新的方法(NSString*) getSkinColor和(void) setSkinColor:(NSString *) color,分別用來設定和讀取我們的執行個體變數skinColor。
好的,我們總結一下繼承的時候的子類的格式。
@interface 類的名字 : 父類的名字 {
實體變數類型 實體變數名字;
}
- (傳回值類型)重載的方法名字;
+ (傳回值類型)重載的方法名字;
- (傳回值類型)其他的方法名字:(變數類型) 變數名字:(變數類型) 變數名字;
@end
4.4,self和super
我們再來開啟“Bull.m”, 在saySomething的定義的部分,我們發現了如下的代碼:
NSLog(@"Hello, I am a %@ bull, I have %d legs.", [self getSkinColor],legsCount);
我們在這句話當中,發現的第一個新朋友是%@,這是在告訴編譯器,需要把%@用一個後面定義的字串來替換,在這裡我們給編譯器提供的字串是[self getSkinColor]。看到這裡,同學們又會發現一個新的朋友self。
在類的方法定義域之內,我們有的時候需要訪問這個類自己的執行個體變數,或者是方法。在類被執行個體化之後,我們就可以使用一個指向這個類本身的一個指標,在Java或者C++裡面的名字叫做this,在Objective-C裡面,這個名字是self。self本身是一個id類型的一個指標變數。我們在第3章裡面講解過,方法的調用格式如下:
[對象或者類名字 方法名字:參數序列];
在類的方法定義域裡面,當我們需要調用類的其他方法的時候,我們需要指定對象或者類的名字,我們的方法是一個執行個體方法所以我們需要一個指向自己的對象,在這裡我們需要使用self。
我們假設,如果方法聲明裡面的參數序列裡面有一個參數的名字和類的執行個體變數發生重複的情況下並且由於某種原因我們無法更改參數和實體變數的名字的話,我們應該如何應對呢?答案是使用self,格式如下
self->變數名字
通過這樣寫,我們可以取得到類的變數的數值。當然如果沒有名字衝突的話,我們完全可以省略self->,Xcode也足夠的聰明能夠識別我們的執行個體變數,並且把我們代碼裡面的執行個體變數更改為相應的醒目的顏色。
如果我們在類的方法裡面需要訪問超類的方法或者變數(當然是訪問對子類來說是可視的方法或者變數),我們需要怎樣寫呢?答案是使用super,super在本質上也是id的指標,所以,使用super訪問變數和方法的時候的書寫格式,和self是完全一樣的。
“Bull.m”裡面的其他的代碼,沒有什麼新鮮的東西,所以筆者就不在這裡贅述了。
4.5,超類方法和子類方法的執行
我們來看一下04-Hello Inheritance.m的下面的代碼片斷
1 id redBull = [Bull new];
2 [redBull setLegsCount:4];
3 [redBull setSkinColor:@"red"];
4 [redBull saySomething];
5
6 Bull *blackBull = [Bull new];
7 [blackBull setLegsCount:4];
8 [blackBull setSkinColor:@"black"];
9 [blackBull saySomething];
第1行的代碼在第3章裡面講解過,我們來看看第2行的代碼。
第2行的代碼實際上是向redBull發送一個setLegsCount訊息,參數為4。我們沒有在Bull裡面定義setLegsCount方法,但是從控制台的輸出上來看, setLegsCount明顯是得到了執行。在執行的時候,我們給redBull發送setLegsCount訊息的時候,runtime會在Bull的映射表當中尋找setLegsCount,由於我們沒有定義所以runtime找不到的。runtime沒有找到指定的方法的話,會接著需要Bull的超類,也就是Cattle。值得慶幸的是,runtime在Cattle裡面找到了setLegsCount,所以就被執行了。由於runtime已經尋找到了目標的方法並且已經執行了,所以它就停止了尋找。我們假設runtime在Cattle裡面也沒有找到,那麼它會接著在Cattle的超類NSObject裡面尋找,如果還是找不到的話,由於NSOBject是根類,所以它會報錯的。關於具體內部是一個怎樣的機制,我們將在後面的章節裡面講解。
第3行的代碼,是設定skinColor。
第4行的代碼是給redBull發送saySomething的訊息。按照第2行的runtime的尋找邏輯,它首先會在Bull類裡面尋找saySomething,這一次runtime很幸運,它一次就找到了,所以就立即執行。同時runtime也停止了尋找的過程,所以,Cattle的saySomething不會得到執行的。
在第6行裡面,我們定義了一個blackBull,但是這一次我們沒有使用id作為blackBull的類型,我們使用了Bull *。從本質上來說,使用id還是Bull *是沒有任何區別的。但是,我們來想象,當我們的程式存在很多id類型的變數的話,我們也許就難以區分究竟是什麼類型的變數了。所以,在沒有特殊的理由的情況之下,我們最好還是顯式的寫清楚類的名字,這樣可以方便其他人閱讀。由於Bull從Cattle繼承而來,我們也可以把地6行代碼改為
Cattle *blackBull = [Bull new];
4.6,本章總結
感謝大家閱讀到這裡!我們在本章學習了:
- 超類,子類的概念以及如何定義和聲明。
- self和super的使用方法以及使用的時機。
- 超類和子類的方法的執行。
我們在下一章將要講述selector等等的一些其他的重要的概念。