Objective-C編程 — 類和繼承

講述面向對象中的一個重要概念——繼承,使用繼承 能夠方便地在已有類的基礎上進行擴展,定義一個具備父 類所有功能的新類。css

父類和子類

咱們在定義一個新類的時候,常常會遇到要定義的新類是某個類的擴展或者是對某個類的修正 這種狀況。若是能夠在已有類的基礎上追加內容來定義新類,那麼新類的定義將會變得更簡單。面試

像這種經過擴展或者修改既有類來定義新類的方法叫做 繼承 (inheritance)。在繼承關係中,被繼

承的類稱爲 父類 (superclass),經過繼承關係新建的類稱爲 子類 (subclass)。編程

繼承意味着子類繼承了父類的全部特性,父類的數據成員和成員函數自動成爲子類的數據成員

和成員函數。除此以外,子類還能夠ide

●  追加新的方法函數

●  追加新的實例變量學習

●  從新定義父類中的方法測試

固然,若是子類中只追加新的實例變量而不變動方法則沒有任何意義。子類中從新定義父類的方法 叫做 重寫 (override)。spa

讓咱們來看幾個例子。在圖 3-1 中,類 B 是類 A 的子類,類 B 繼承了類 A 的實例變量和方法, 但重寫了 method2。類 C 也是類 A 的子類,類 C 中增長了新的實例變量 z 和新的方法 method3。類 B 和類 C 都是類 A 的子類,不管類 A、類 B 和類 C 的任何一個實例變量都可以執行方法 method1 和 method2。設計

 
enter image description here

父類和子類是一種相對的稱呼。例如,在上例中,若是以類 B 爲父類又派生出一個子類 D,那 麼類 B 相對於類 A 是子類,相對於類 D 卻爲父類。3d

另外,在集合用語中,子集指的是比較小的集合(相對於父集),但在類的狀況下子類通常是父 類的擴展。爲了不這種命名上的混亂,C++ 中把父類稱爲 基類 (base class),把子類稱爲 派生 類 或 導出類 (derived class)。考慮到面向對象的程序設計中通常都使用父類、子類的叫法,本書也 使用這種叫法。

一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:1012951431, 分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。

類的層次結構

假如以某個類爲父類生成若干子類,而後再繼承這些子類並生成更多的子類,如此循環下去就 可能會生成一顆倒立的樹,它由經過繼承而彼此關聯的類組成,這樣的樹稱爲 類層次結構 (class hierarchy)。

位於類層次最頂端的類稱爲 根類 (root class),如圖 3-2 所示。

 
enter image description here

NSObject 是 Cocoa 環境下的根類,Cocoa 中全部的類都直接或間接地繼承了 NSObjectA。新建的 任何類都必須是 NSObject 或它的繼承類的子類。NSObject 中定義了全部 Objective-C 對象的基本 方法。

因爲這種類的層次關係,Objective-C 的全部對象都繼承了 NSObject 類中定義的各類屬性。 Objective-C 的對象可以做爲對象來使用,就是由於類 NSObject 中定義了對象的基本功能。 在面向對象的語言中,有的和 Objective-C 同樣有惟一根類,例如 Java 和 Smalltalk 等;有的則不 存在惟一根類,如 C++。

利用繼承定義新類

繼承的定義

若是想經過繼承爲某個類定義一個子類,該怎麼辦呢?

Objective-C 在子類的接口部分聲明繼承關係。在 2.2 節中咱們已經說明了如何定義類的接口,這 裏再介紹一遍。

 

定義父類 A 的子類 B 的時候,「類名」是新類 B,冒號後面的「父類名」是須要繼承的類 A。

至此爲止本書中的父類都使用了 NSObject,這是由於 Objective-C 中全部的類都要繼承根類, 而  NSObject 是  Objective-C 中全部類的根類 。若是子類有想繼承的類,就要直接指明該類爲父類,否 則就須要指定 NSObject 爲父類。前文中定義 Volume 類的時候,由於 Volume 類並無特別想繼承的 類,因此直接使用了 NSObject 做爲父類。

實例變量的聲明中只須要聲明新增的變量。若是沒有新增的變量,則只須要加上 {} 便可,有時 甚至連 {} 均可以省略。 方法的聲明中只須要追加新增的方法。若是要覆蓋父類中已聲明的方法(重寫),則須要在接口 中對方法從新聲明。一般咱們會給重寫的方法加上註釋,以便理解。

下面展現了定義類 A 的子類 B 時接口部分的狀況。變量 x 和方法 method1 繼承於類 A,因此不 須要從新聲明,方法 method2 的聲明也能夠被省略。

 

類定義和頭文件

假設有一個已經定義好了的類 Alpha,那麼頭文件 Alpha.h 就應該已經存在。要定義類 Alpha 的 子類 Beta 的時候,頭文件 Beta.h 中必須包含 Alpha.h。不知道父類定義的話是沒法定義子類的。因此 包含父類接口的頭文件是必須的。

類的實現部分必須引入包含類的接口部分的頭文件。實現部分須要包含新增和重寫的方法的實 現。固然實現部分也能夠定義各類局部函數和變量。

圖 3-3 的文件 Gamma.m 的方法中調用了方法doSomething ,這個方法是從類 Alpha 繼承而來 的。 文 件 Gamma.m 引 入 的 頭 文 件 Gamma.h 中 引 入 了 Beta.h,Beta.h 中 又 引 入 了 Alpha.h, 所 以 Gamma.m 能夠調用方法doSomething。

 

類的定義能夠不斷地使用繼承向下擴展,但不管怎麼擴展,只要保證了這種頭文件的引入方式, 任何一個派生類中就都能使用父類中定義的變量和方法。

繼承和方法調用

子類中定義的方法,除了可以訪問新追加的實例變量外,也可以訪問父類中定義的實例變量。

另外,由於繼承的緣由,子類也能夠響應父類中定義的消息。但若是子類中重寫了父類的方法, 就須要注意實際運行中到底哪一個方法(父類的仍是子類的)被執行了。

如圖 3-4 所示,類 A 包含方法 method一、method二、method3。類 B 是類 A 的子類,類 B 中從新 定義了 method2。類 C 是類 B 的子類,類 C 中從新定義了 method1。

 

咱們來看看給類 B 的實例變量發送消息時的狀況。首先,假設向類 B 的實例對象發送了對應 method1 的消息,即進行了方法調用。雖然類 B 中沒有 method1 的定義,但由於類 B 的父類類 A 中 定義了 method1,因此會找到類 A 的 method1,調用成功。消息 method3 的狀況下也是一樣的道理, 類 A 中定義的 method3 會被執行。method2 同前兩個消息不一樣,類 B 中定義了 method2,因此會使 用自身定義的 method2 來響應這個消息。

而給類 C 的實例發送消息的話會怎麼樣呢?類 C 中有 method1 的定義,因此會直接使用類 C 中 定義的 method1 來響應這個消息。類 C 中沒有 method2 的定義,因此調用的時候會使用類 B 中定義 的 method2 來響應。類 C 和類 B 中都沒有定義 method3,因此類 A 中的定義 method3 會被調用。

調用父類的方法

子類繼承了父類以後,有時就可能會但願調用父類的方法來執行子類中定義的其餘處理,或者 根據狀況進行和父類同樣的處理或子類中單獨定義的處理。讓咱們來看看圖 3-4 中的例子,若是要在 類 B 的 method2 的定義中調用類 A 的 method2,那麼該怎麼辦呢?經過 self 調用 method2 的話,就 會變成遞歸調用自身定義的 method2。

若是子類中想調用父類的方法,能夠經過 super 關鍵字來發送消息。使用 super 發送消息後,就 會調用父類或父類的父類中定義的方法。如圖 3-5 所示,類 C 中定義了 method1 和 method3。類 C  的 method1 中經過 super 調用了 method3,這時被調用的 method3 是類 A 中定義的 method3。

 

super 和 self 不一樣,並不肯定指向某個對象。因此 super 只能被用於調用父類的方法,不能經過 super 完成賦值,也不能把方法的返回值指定爲 super。

初始化方法的定義

新追加的實例變量有時須要被初始化。另外,子類也可能須要同父類不一樣的初始化方法。這些 狀況下就須要爲子類定義本身的初始化方法。

子類中重寫 init 初始化方法的時候,一般按照如下邏輯。其餘以 init 開頭的初始化方法也是同理。

 

請注意第一行調用了父類的init 方法,父類的init 方法會初始化父類中定義的實例變量。下 面是子類專有的初始化操做。

若是全部的類的初始化方法都這樣寫,那麼根類 NSObject 的init 方法就必定會被執行。不然 生成的對象就沒法使用。與此同時,這樣作也能夠防止漏掉父類中定義的實例變量的初始化。

執行的時候父類的初始化方法可能會出錯。出錯時則會返回 nil,這種狀況下子類也不須要再進 

行初始化,直接返回 nil 就能夠了。

若是父類是 NSObject,則基本上不可能初始化出錯,所以不判斷這個返回值也是能夠的。使用 傳入的參數或經過從文件讀入變量進行初始化時,由於值的類型錯誤或讀取文件失敗等緣由,初始 化有可能會失敗。這種狀況下,須要確認父類的初始化方法的返回值。另外,上例中對 self 進行了 賦值,關於這個賦值的含義咱們會在第 8 章中詳細說明,這裏只須要記住這是初始化方法的一種固 定寫法便可。

生成實例對象的方法alloc 會把實例對象的變量都初始化爲 0(後面會提到的實例變量 isa 除 外)。因此,若是子類中新追加的實例變量的初值能夠爲 0,則能夠跳過子類的初始化。可是爲了明確是否能夠省略,最好爲初值可爲 0 的變量加上註釋。

從程序的書寫角度來講,設定初始值的方法有兩種,便可以在初始化方法中一次性完成實例變量 的初始化,也能夠在初始化方法中先設置實例變量爲默認值,而後再調用別的方法來設置實例變量 的值。例如,類 Volume 也能夠經過先調用初始化方法init ,而後再調用setMax: 等方法來設定音 量的最大值、最小值和變化幅度。原則上來講,初始賦值以後值再也不發生變化的變量和須要顯示設 定初值的變量,都須要經過帶參數的初始化方法來進行初始化。

使用繼承的程序示例

追加新方法的例子

咱們來定義一個帶有靜音功能的類 MuteVolume。該類只有一個功能,即當收到mute 消息時, 設置音量爲最小。

類 MuteVolume 的定義很是簡單,父類是已經定義好的類 Volume。子類 MuteVolume 除了可使 用父類 Volume 中定義的全部實例變量和方法以外,還新增長了一個 mute 方法。

 

這裏使用了 Volume 做爲父類,並引入了頭文件 Volume.h。Volume 的父類是 NSObject,因此 , 因此就不須要再進行指定了。

沒有定義新的實例變量,意味着子類中沒有要追加的實例變量。

 
 

該測試程序的功能是從終端讀入輸入的字符串,並根據字符串的第一個字符來決定如何設置音 量。具體來講,第一個字符爲 u 時表示提升音量,d 表示下降音量,m 表示靜音,q 表示退出程序。

編譯子類的時候,須要連同父類一塊兒編譯和連接,不然就沒法使用父類中定義的方法。本例中

編譯所須要的文件一共有 5 個,即 Volume.h、Volume.m、MuteVolume.h、MuteVolume.m、main.m。

 

方法重寫的例子

上面經過繼承實現靜音功能類的例子很是簡單,讓咱們來看一個更實用的例子。

假設該例子要實現兩個功能。第一個功能是,當再次收到mute 消息時,音量會恢復原值;第二 個功能是,在靜音狀態下收到up 或down 消息時,會返回最小音量值,同時改變音量值。

實現這些功能的方法有不少,這裏咱們增長一個 BOOL 類型的變量 muting,同時修改方

法initWithMin:max:step:和 方法value的 實現。

 
 
 

初始化方法initWithMin:max:step:首 先調用了父類的初始化方法,而後對新增的實例變量 muting 進行了初始化。如前所述,子類的初始化必定要在父類的初始化以後進行。

value方 法根據當前是否爲靜音狀態返回不一樣的值。靜音狀態下,返回最小值 min。mute 方法 

中只須要改變實例變量 muting 的狀態來標識是否靜音,不須要更改音量值 val。

編譯的狀況和上一節同樣。main.m 直接使用上一節的便可。

繼承和方法調用

使用 self 調用方法

若是想在一個方法中調用當前類中定義的方法,能夠利用 self。但若是存在繼承關係,經過 self 調用方法時要格外注意。

在圖 3-6 的例子中,有三個類 A、B、C。類 A 中定義了 method一、method2 和 method3 三個方法。 類 B 繼承了類 A,重寫了 method1 和 method3。類 C 繼承了類 B,重寫了 method2。

 

假設類 B 的方法 method3 想調用 method1 和 method2,經過 self 調用了 method1 和 method2。我 們來分析一下這個過程當中到底哪一個函數被調用了。對類 B 的實例對象調用 method3 方法時,首先會 經過 self 調用 method1,這個 method1 就是類 B 自身定義的 method1。接着,method3 經過 self 調用 method2,由於類 B 中並無 method2 的定義,因此就會調用從類 A 中繼承而來的 method2。

而若是是類 C 的實例對象調用方法 method3 的話會怎麼樣呢?咱們首先來看看 method3,由於 類 C 中並無定義 method3,因此調用的是類 B 中定義的 method3。要注意這個時候 self 指的是類 C 的實例對象,當 [self  method1] 執行時,由於類 C 中沒有定義 method1,因此調用的是類 B 中 定義的 method1。而後,當 [self  method2] 執行時,由於類 C 中定義了 method2,因此執行的是 類 C 中定義的 method2,而不是上例中類 A 中定義的 method2。另外還有一點須要注意,就算類 B 中定義了 method2,調用的也是類 C 中定義的 method2。

也就是說,self 指的是收到當前消息的實例變量 ,所以,就算是同一個程序,根據實例的類的不 同,實際調用的方法也可能不相同。

使用 self 的時候要必定當心,要仔細分辨到底調用了哪一個類的方法。即使如此,利用 self 的特 性來編程也是很常見的,更多詳細內容請參考 11.1 節的內容。

使用 super 調用方法

而若是不使用 self 而使用 super,程序執行的結果會怎樣呢?

圖 3-7 是用 super 替代圖 3-6 中的 self 的狀況。使用 super 調用方法時,最後被調用的方法是類 B 的父類中定義的方法。因此不管是類 B 仍是類 C 的實例變量調用了 method3,最後調用到的都是類 A 中定義的 method1 和 method2。

 

測試程序

咱們用一個簡單的程序來驗證一下上面所描述的內容。這個程序自己並無太大的意義,僅僅 是用來測試方法調用的。

測試程序中有三個類 A、B、C。類 A 中定義了方法 method1 和 method2。類 B 中對 method1 進 行了重寫,經過 self 調用了 method1,經過 super 調用了 method2。類 C 重寫了 method1。

 
 

程序執行以後輸出以下。能夠看出,類 B 和類 C 的實例分別調用了不一樣的方法。

 

方法定義時的注意事項

局部方法

實現接口聲明中的方法時,可把具有獨立功能的部分獨立出來定義成子方法。通常狀況下,這 些子方法都只供內部調用,不須要包含在類的接口中對外公開。

這種狀況下,局部方法能夠只在實現部分(一般是 .m 文件)中實現,而不須要在接口部分中進 行聲明。這樣一來,就算其餘模塊引用了接口文件,也沒法得到這個方法的定義,沒法調用這個方 法,從而就實現了局部方法。但這裏只是說沒法從接口中得到這個方法的定義,這個方法自己仍是存在的,只要發送了消息,就可以執行。

讓咱們來看一個簡單的例子,類 ClickVolume 是類 Volume 的一個子類,它的主要功能是當音量 發 生 變 化(提 高 或 降 低 )時 發 出 提 示 音。 提 高 或 降 低 音 量 時 發 出 提 示 音 使 用 一 個 共 同 的 方 法playClick, 定義以下所述。由於這個功能不會在其餘地方使用到,因此咱們把它定義成一個局 部方法,不在接口文件中聲明。

 

未在接口中聲明的局部方法和沒有進行屬性聲明的 C 語言函數同樣,只能被定義在局部方法之 後的方法調用。在上面的例子中,playClick 就必須定義在up 和down 的前面。定義順序方面出現的 問題,可使用第 10 章介紹的「範疇」(category)來解決。

編程的時候使用局部方法能夠加強程序的可維護性,但在繼承的時候可能會出現問題。例如, 子類新追加的方法可能並不知道父類已經實現了局部方法而去從新實現一個父類的局部方法。

爲了不這一問題,蘋果公司建議爲局部方法名添加固定的前綴(詳情請參考附錄 C)。

指定初始化方法

前面已經介紹過了如何定義初始化方法,但還有一些要注意的地方。

根據需求有時可能須要爲一個類定義多個不一樣的初始化方法。例如,既須要提供一個可指定每 個參數初始值的初始化方法,又須要提供一個每一個參數都直接使用默認值的初始化方法;既須要提供 一個用內存變量進行初始化的初始化方法,又須要提供一個能從文件讀入變量完成初始化的初始化 方法等。 指定初始化方法 (designated initializer)就是指能確保全部實例變量都能被初始化的方法, 這種方法是初始化的核心,類的非初始化方法會調用指定初始化方法完成初始化。一般,接收參數 最多的初始化方法就是指定初始化方法。

子類的指定初始化方法一般都是經過向 super 發送消息來調用超類的指定初始化方法。除此以外, 還有一些經過封裝來調用指定初始化方法的方法叫做 非指定初始化方法 (secondary initializer)。圖 3-8展現了指定初始化方法的概念,箭頭指明瞭調用關係。圖中每一個類都只有一個指定初始化方法,實 際上也能夠存在多個。

子類的指定初始化方法,必須調用超類的指定初始化方法。如圖 3-8 中所示,按照類層次從底向 上,各個類的指定初始化方法會被連鎖調用,一直到最上層的 NSObject 的指定初始化方法——init 爲止。

 

若是子類中想重寫父類中的指定初始化方法,就必定要調用父類的指定初始化方法,而不能調 用父類的非指定初始化方法。緣由是非指定初始化方法內部會調用指定初始化方法,形成遞歸循環 調用,沒法終止。

請看圖 3-9 中的例子,類 A 的指定初始化方法是initWithMax :。init 是類 A 的非指定初始 化方法。類 B 是類 A 的子類,在 B 中重寫了指定初始化方法initWithMax :。initWithMax :中 調用了父類類 A 的 init。如圖所示,若是類 A 的 init 中經過 self 調用了initWithMax :,那麼,當 初始化對象是類 B 的實例時,就又會調用到類 B 的initWithMax :,這樣就變成了一個遞歸循環, 調用永遠沒法結束。

 

再讓咱們回頭看一下圖 3-8,圖 3-8 中類的非指定初始化方法都調用了指定初始化方法來進行初 始化,同時父類的非指定初始化方法也能夠被繼承,但定義的時候必定要注意,不然也會出現循環 調用的問題。

Objective-C 沒有特殊的語法或關鍵字來代表哪一個方法是指定初始化方法,因此一般須要經過 文檔或註釋來標明指定初始化方法。Cocoa API 文檔中的絕大多數類都標明瞭哪一個方法是指定初始 化方法。

另外,若是你想一塊兒進階,不妨添加一下交流羣1012951431,選擇加入一塊兒交流,一塊兒學習。期待你的加入!

 
相關文章
相關標籤/搜索