close

本章學習目標:

1. 了解 Objective-C 和 C 語言的關係。    

2. 了解 Objective-C 在 iPhone SDK 裏扮演的角色。

3. 能夠利用 Objective-C 建立一個屬於自己的類別(class)。

 

Objective-C 是開發iPhone時主要使用的語言,顧名思義,這個語言是由 C 語言擴充而來,同時提供了 C 語言所沒有的「物件」這個概念,由字面上來說 Objective 可以將它翻譯成「物件導向的」 ,而 C 則是這個語言的基礎,Objective-C 就字面上來說就是「這是一個以 C 語言為基礎發展而成的物件導向語言,因此我們可以把 Objective-C 想像成是 Apple版本的物件導向 C 語言。 也因為它是以 C 為基礎的語言,所以原來的 C 語言,也可以用來開發 iPhone 的程式,事實上,在 iPhone SDK中就有不少的 framework 是提供 C 語言的宣告。 例如專門負責 2D 繪圖的函數庫 Quartz 2D提供的標頭檔(header file,即.h檔),裏面的的宣告就是使用 C 語言。但是在 iPhone SDK 大部分的 framework 都是使用 Objective-C 實作的情況下,熟悉 Objective-C 自然是 iPhone 程式設計入門必修的第一堂課 


 

2.1 物件導向設計(OOP) 

傳統的程式設計分成二個主要的部分,一是資料,另一個則是行為,對應到現實世界中,資料可想像成是一些事物如書本、人、房子等等。行為則指的是做什麼事,例如:吃、走、坐等。在物件導向設計的概念出現之前,資料和行為之間是沒有必然的關係的。所以在程式設計時若是規劃不好,執行的結果出現房子在飛,書在走路等奇怪現象是有可能的。

物件導向最重要的概念就是將資料和行為合而為一,為各種資料定義它的行為,決定資料能做什麼處理,為每件東西定義和它相關的行為。這種將資料和行為合而為一的概念,在物件導向中就稱為物件。以物件為出發點來設計程式,只要經過適當地規劃,就可以避免許多錯誤的邏輯發生,而我們更可以用常見的 4W(What,Why,How,Where)來幫助我們規劃合適的物件,所以說物件導向設計實際上是比較符合我們對現實世界的思維的。底下的例子就是在設計一個物件時可以使用的方式。

What 這個物件是什麼? 它有什麼功能? 它的角色是什麼?
Why 為什麼要有這個物件,難道不能用其它的物件來取代它的工作嗎? 它是有什麼特殊的能力是其他物件做不到的?
How 這個物件要提供什麼函數,才能達到它該有的功能。
Where這個物件和其他的物件要如何合作和分工,他們的分界線在哪? 什麼功能該由我提供,什麼功能該讓別的物件提供?

以上的問題,是否就很像是我們平常在思考問題的方式呢?在設計一個物件時,反覆地為這個物件思考它在這4個問題中的角色,在設計一個合適的物件的過程中是很重要地。

當然,以上的問題只是提供設計時的一個參考,真正的準則還是得因地置宜,因當時的需求而定。


 

2.2 學習 Objective-C 前必須俱備的C語言基本知識

C 語言是 Objective-C 的前身,許多在C語言中的語法例如註解、變數宣告、迴圈等等,都可以直在套用在Objective-C中,因此在學習Objective-C之前,C語言中一些基本的知識是必須要了解的,這一節將為大家複習Objective-C中幾個常見於C語言中的重要觀念。

 

2.2.1 變數

變數指的是在電腦記憶體中的一塊記憶體,我們可以對它進行讀取、寫入的動作並在程式中使用它。每一個變數都會連結到某一種資料型別,這些型別決定了這些變數佔用了多少記憶體。通常我們在程式中會使用到變數的幾個數值:

值(Value

指的是變數的內容,例如說有一個變數,他的型態是整數而值是5,我們就會說這個整數變數的值是5

位址(Address

指的是變數儲存在記憶體的位址。

 

 

2.2.2 變數的種類

我們依照變數所處的位置,將變數區分成下面三個類別:

 

變數種類

說明

區域變數

( local variable)

僅存在於函數裏面,當程式進入某個函數後,系統才會為該函數的區域變數配置記憶體,當函數執行完成後,系統會將這些記憶體釋放出來提供他人使用。

參數

(parameter)

當某一個函數有參數的時候,我們必須使用一些變數來提供這些參數儲存資料的地方,這種變數我們稱之為格式參數或直接稱為參數。

全域變數

(global variable)

全域變數可供整個程式中的所有成員存取,任何在程式中的運算式都能夠存取到全域變數,系統會為全域變數在整個程式的執行階段都保留它們的值。

因為系統會為全域變數特別配置記憶體,這些記憶體在整個程式的執行過程都不會被釋放,但實際上因為手機上能使用的記憶體非常有限,所以雖然全域變數在使用上有其方便性,我們仍需儘量避免使用全域變數。

 

 

2.2.2資料型別與常數

所有在C中支援的原生型別,在Objective-C都能夠直接使用。以下是5種基本的資料型態。

型態

典型長度(位元)

說明

int

32

整數

char

8

字元

float

32

浮點數

double

64

雙精準浮點數

void

32

無值,無型態。

雖說以上是一般情況下這些變數的長度,但是實務上,變數真正佔用的位元數是和執行的平台相關的,因此若要取得變數所佔的位置數必須使用「sizeof()」來取得變數真正的長度才能避免程式執行中發生不必要的錯誤。

註:

在實務上,除了上述幾個基本型態之外,還有另一種常見型態稱為「布林」,布林值代表的意義:是「真」、「假」。
在實作上可以使用:


1.
「1」     代表「真」,「0」代表「假」。
2. 「非0」代表「真」,「0」代表「假」。

在C系列語言中常見用來代表布林值的符號是BOOL或是bool。

 

除了5大基本型態之外,C語言也有使用額外的修飾字來延伸4種基本型態(void沒有額外的修飾辭),以下則是這些修飾辭使用的通則,必須注意的是,並不是所有的變數都適用於這些修飾辭,某些修飾辭僅適用於某些型態的變數而已,而且在某些平台上這些修飾辭甚至沒有任何作用。

修飾辭

說明

適用

signed

變數範圍包含正負值

int,char

unsigned

變數範圍只含正值

int,char

long

變數範圍為原型態的兩倍

int,char,double

short

變數範圍為原型態的一半

int

 

常數指的是程式無法改變的固定值。常數可以是任何基本型態之一。例如整數常數: 10,浮點數常數:3.1416等都是常數。

 

2.2. 什麼是指標?

指標是在C語中必須了解的一個重要概念,而在Objective-C中尤其重要,在整個iPhone 程式的開發過程,所有iPhone SDK物件都必須依賴指標的傳遞。精通使用指標的方法是開發iPhone程式的必修課程。

指標是一種特殊的變數,它儲存的內容是記憶體的位址,而這個位址是這個指標指向的資料真正儲存資料的地方。指標就像是地址一樣,告訴你某人住在哪,而這個地址可能會是他的家,或是辦公室。而你可以透過地址找到某個人。

在使用指標時,最重要的地方就是指標必須指向一個有效的物件,否則誤用這個指標將會造成無法預期的結果,可能會造成系統異常,甚至會有更嚴重的事情發生。

以下是宣告指標的一個例子:

int * piPointerSample;

其中「*」代表著後面跟隨的變數是一個指標。下面我們用圖示來解釋這一行程式碼的意義:

  

 

以此例來說,piPointerSample一個指向整數的指標。前面的int表示這個指標指向的位址儲存了一個整數變數。

和指標相關的運算子共有下面兩個:

 

運算子

說明

傳回一個變數的位置。

取得指標變數所指向的內容。

若我們對指標變數使用「*」運算子,則「*piPointerSample」代表的是一個整數變數。而「&」則是讓我們取得某個整數變數的位址,讓我們能利用這個運算子取得某個變數的位址然後指定給一個指標變數。

下面就是將一個整數變數指定給piPointerSample和從piPointerSample將值取出來的例子。

01 int iVariReturnValue;
02 int * piPointerSample = &iVar;
03 iReturnValue = *piPointerSample; 

 

-              在上面的第一行中,我們宣告了兩個整數變數:iVar iReturnValue

-              在第二行中,我們對iVar使用「&」運算子表示我們想要得到iVar 這個變數在記憶體中的位址,然後把這個位址指定變piPointerSample這個指標變數。

-              在第三行中,我們對piPointerSample這個指標變數使用「*」運算子來取得piPointerSample這個指標變數指向的整數值,然後再將這個整數值指定給iReturnValue這個變數。

 

2.2. 參數的傳遞(Argument Passing)

當函數有參數時,參數傳遞的方式,分成以下兩種:

傳值法(Pass by value,Call by value。)

傳值法的參數會在函數的中產生一份新的變數複本,在函數中任何對此複本所做的操作都不會影響到原來的變數。而在離開函數後這個將會複本所佔用的記憶體將被釋放。

傳參考法(Pass by reference, Call by reference。)

在函數裏不會建立變數的複本,而是直接將變數的位址傳入函數中,所有在函數中對這個變數的操作都會影響到原來的變數。

傳值法和傳參考法並沒有所謂的優劣之分,傳值法因為不會影響到原來的變數,意謂著在函數中任何的不小心錯誤地修改到傳進來的變數,都是對變數的複本做修改,程式因此受到的傷害相對較小。但是對於比較龐大的變數來說,傳值法會對系統造成比較大的負擔(因為製作複本的成本較高),因此傳參考法會是較好的選擇。

傳值法和傳參考法最大的差別在於在函數中「直接」對傳入參數的修改,是否會影響到傳入參數,當參數使用傳值法時,任何在函數中對該參數的修改,都不會影響到原來的參數。而使用傳參考法時,任何的修改都會直接作用在原來的參數上。

Objective-C繼承了C的特性,在程式的參數宣告中,並沒有像C++語言提供「&」這種reference型別的參數,因此我們無法在Objective-C中,「直接」使用傳參考法的方式來傳遞參數 。

 

註:有件事需要特別注意的,這個「&」存在於函數的參數宣告中,和前面提到的「&」指標運算子代表著不同的意義。

 

下面是一個宣告這種參數傳遞函數的例子

int CallByReferenceTest(int & piParam);  // C++語言支援此語法。

上面的宣告能讓使用者「直接」將參數的位址傳入函數中,但在Objective-C中函數的參數傳遞並不支援上面這種函數的宣告,也就是我們無法使用上面的宣告來將變數的位址「直接」傳入函數中。如果我們希望某一些變數能夠在函數中被修改,就必須使用「間接」的方式,利用傳遞該「變數的指標」,也就是利用指標的「&」運算子取得該變數的位址,然後在函數中利用指標運的「*」運算子取得變數儲存的位址「間接」地來修改變數的值。也就是說,如果在 Objective-C中想要有類似傳參考法的參數傳遞的效果,必須使用類似以下的語法;

int iVar = 0; 
PassByReferenceTest(&iVar);

其中 CallByReferenceTest 這個函數的宣告是:

int PassByReferenceTest(int * piParam);

在宣告中的「int *」表示這個函數參數的「型態」是「整數指標」,而 piParam 就是一個「整數指標的變數」,在 CallByReferenceTest 這個函數中,因為參數是一個整數指標,所以piParam 儲存的資料將會是iVar這個整數變數的「位址」,這個位址是利用傳值法傳進來的,因為我們是將「值」傳進來,並在函數中產生一個複本(piParam)來儲存這個值,在函數中任何對piParam「直接」的修改都不會影響到原來傳進來的變數值。

這時,利用指標的運算元「*」 就可以「間接」地修改我們打算修改的變數值:iVar,舉個例來說如果函數的實作如下:

int CallByReferenceTest(int * piParam)
{
     *piParam = 5;
     return;
}

在上面的例子中,我們進行了下面幾件事來達到在函數中修改iVar變數過程:

  1. 利用「&」運算子取得iVar這個變數的位址。 
  2. 將取得的位址傳入PassByReferenceTest函數。 
  3. PassByReferenceTest函數會產生一個整數指標的變數(piParam)。 
  4. iVar的位址(步驟1的結果)存入piParam
  5. 在函數中我們利用指標運算子「*」取得指標指向真正資料的記憶體位址,這時*piParam將會指向iVar所在的記憶體位址的整數變數。
  6. 5填入該變數。這時iVar的值就會變成5
  7. 離開函數後,系統釋放piParam所佔用的記憶體。

 

註:

將變數用指標傳入函數的方式,坊間有些中文書籍稱之為 Call by address 或是 Call by pointer,但是此種參數傳遞的方式本質上仍是Call by value。只不過此時的value是一個變數的位址(address),所以要存取或修改真實的變數值,必須使用指標方式來存取。

 

 

2.2.4 指標的指標

除了基本的指標以外,另一個容易讓人困擾的觀念就是「指標的指標」。下面就是一個常見的例子。

int ** ppMySample;

乍看之下,這種宣告當場令人儍眼,此時可以將上面的宣告拆成兩部分來幫助我們理解上面的式子,現在請將上面的式子換個想法看成:

int * * ppMySample

第一個部分是「int *」,這個宣告由前面小節的介紹,我們知道這是一個整數指標,換個角度來看,我們將這個東西看成是另一種新的資料型態,而這個型態就叫做「整數的指標」,我們估且將它取名叫做newtype,這時,上面這一句我們就可以把它想像成是:

newtype * ppMySample

再套用一次指標的定義,上面的式子就是「newtype的指標」,現在,我們將newtype還原成它原來的敍述,上面的式子就變成了「整數的指標」的「指標」,如此一來我們就完成了一個指標的指標的敍述。雖然這個敍述看起來很奇怪,但是在實務上,這種例子很常見,指標的指標之所以存在,和C語言只允許傳值法(請參考前一小節的說明)有很大的關係。怎麼說呢?因為「指標的指標」存在的目的,就是為了讓函數有修改指標的能力。因為C語言只允許將參數的值傳入函數中,所有在函數中對變數的修改都只會影響到函數中的變數複本而不是原來的變數,這時若我們想要修改一個指標的值該怎麼辦呢?參考前面的介紹,若我們想要在函數中修改傳入參數的值,就必須使用「間接」的方式,將該變數的位址傳入函數中,然後利用指標的「*」運算子來修改該變數的值。

舉個例子來說,「char *」常用來指向C語言中的如下的字串:

char * szMyFirstString = “This is my first string.”

這時如果在函數中,我們需要把「szMyFirstString」指到另一個字串「szMySecondString」要如何辦到呢?因為我們想要修改的東西是「char*」,如果直接以「char *」為參數傳入函數內我們並沒有辦法修改「szMyFirstString」所指向的內容,此時就必須依賴「指標的指標」來幫助我們達成目標。就如同下面的例子:

void ModifyString(char ** ppInputString)
{
     *ppInputString = “This is my second string”;
}

請將上面例子中的「char*」,視為一個完整的個體,修改szMyFirstString的方式,就是利用&運算子將szMyFirstString的位址傳入函數中。如:

ModifyString(&szMyFirstString);

當szMyFirstString所代表的型態「char *」以「&」取得位址之後,傳入的參數就是「char**」(char* 的指標就是 char**),此時在函數中就能夠修改「char *」變數所指向的目標了


 

2.2.5 前置處理器指令(Preprocessor directves, 編譯器 directives

我們可以在程式碼中放入一些對編譯器的指令,這些指令我們稱之為前置處理指令或是編譯器指令。在C語言中,這些指令以「#」開頭,在C語言中有許多前置處理指定,舉例來說「#include」和「#define」是兩個常見的兩個指令,前者用來將指定檔案滙入目前的檔案,後者則是常用來定義一些常數或是一些常用的巨集指令。

 

2.3 Objective-CC/C++語言的關係

Objective-C 編譯器允許程式中將C和Objective-C或是C++和Objective-C混用,使用「ObjetiveC」+「C++」語法的程式稱之為Objective-C++,通常只有為了和原有的C++程式或是C++函數庫整合時才會使用Objective-C++,一般來說iPhone的程式開發仍是以Objective-C為主。

XCode中使用副檔名來判斷程式使用的語言是 C 或是 C++,Objective-C的檔案可以使用以下副檔名:

副檔名

說明

h

標頭檔,或稱定義檔或是介面檔。標頭檔包含了類別、變數、方法和常數的宣告。

m

實作檔,編譯器 將 .m檔視為使用C語言實作,在.m檔中可以混用Objective-C 和 C語言的語法。

mm

實作檔,編譯器 將 .mm 檔視為使用 C++語言實作,在.mm檔中可以混用Objective-C 和 C++ 語言的語法。

 

註:iPhone SDK中有些函數庫就是直接使用C的語法,如 Quartz 2D 和 OpenGL ES的Sample 就提供許多Objective-C和C混用的例子 

 


2.4 Objective-C iPhone SDK

對於一個技藝純熟,凡事喜歡砍掉重練的C Programmer來說,學習Objective-C也許不是一件絕對必要的事但是一般說,學習Objective-C的目的和在寫Windows程式時,去學 .NET framework或是 MFC是一樣的。畢竟從頭開始打造一支程式是件非常耗時耗力的工作,因此善用現成函數庫能有效地提高生產力,甚至是降底錯誤的發生率底下是在開發iPhone程式裏用來製作使用者介面常用的函數庫Cocoa touch。 Cocoa touch包含了兩大函數庫:

 

函數庫

說明

UIKits

這個函數庫中的類別都是以UI開頭,這個函數庫提供了許多元件讓我們能組成程式的使用者介面。

Core Frameworks

這個函數庫中的類別都是以NS開頭,這個函數庫提供了許多建構程式所需要的基本類別,例如:陣列,字串等等。

底下是Cocoa touch的示意圖。 

Cocoa touch  architecture of iPhone OS 

 

在整個iPhone OS架構中,UIKit和Core Frameworks即是使用 Objective-C來開發

而底下則是 UIKit 的架構圖:

UIKit class hierarchy  

 


UIKit 是打造iPhone使用者介面的重要工具,裏面所有的類別都是使用 Objective-C 開發的,因此為了能夠讓程式能和UIKit等iPhone SDK中提供的函數庫做最好的整合,學習Objective-C自然是不二的法門。接下來的幾個章節將開始引導各位進入Objective-C的世界。讓我們開始踏入iPhone程式開發的第一步吧。


 

2.5 物件,類別及訊息傳遞(ObjectsClassesMessaging)

這一節將介紹如何利用Objective-C實作物件,類別及訊息傳遞等最基本的概念。

 

2.5.1 物件(Object)

Objective-C既然稱作物件導向語言,想當然爾,「物件」自然是這個語言裏最核心也是最重要的觀念。

談到物件,簡單地來說就是為了一群資料定義它的行為,它們能做什麼事或是能怎麼樣修改這些資料。在Objective-C裏面,這些行為叫做方法(methods),而它們作用的目標(資料)則稱之為實體變數(instance variables)。物件則是將負責將這些「資料」和「行為」整合在一起的元件。

 

註:Instance variables 中文稱為實體變數,這些變數會在物件正式被系統產生時才會配置對應的記憶體空間,這些變數亦可稱為成員變數(member variables)。

 

Objective-C裏的實體變數都會定義它的存取範圍。一般來說外部的物件並不會(該)直接去存取這些變數,而是透過該物件提供的方法來存取,這些方法可以透過在物件的宣告中定義專屬於物件的特性(Property)來讓系統幫我們產生而不必由我們親自動手來撰寫。

雖然在物件中有些變數是允許給外部存取的,但是習慣上我們通常不會直接去存取這些變數,而是透過特性來存取。因為利用特性存取這些變數能夠提供我們許多額外的好處,包括了簡化物件的記憶體操作、在多執行緒下的變數存取的管理等等。因此,在實務上,即使某個物件將它的某個變數開放出來讓我們使用,我們仍應該先檢查該變數是否有對應的特性可以使用,如果沒有,而又必須對該變數做操作時才會對變數直接做操作。

 

註:Methods中文稱為函數、方法,在其他程式語言中也稱做function。

 

註:當程式設計人員對物件進行了妥善的規劃之後,通常不想讓外界存取的變數都會用適當包裝,讓外界只能透過Property(特性)來存取,但是我們仍有機會遇到一些比較不小心的設計人員,他們定義了Property但是卻沒有適當地將Property對應的變數藏好,讓我們有機會直接存取到這些變數。因此,原則上我們在存取資料的優先順序上,仍是先以利用Property優先,若沒有Property才會試著去存取變數本身。

舉個例來說,我們可能遇到某個物件定義了一個readonly (唯讀)的Property,但是設計者卻不小心把該Property對應的變數開放出來給我們使用,這時如果我們直接對這個變數進行修改就有可能因為超出了當初這個物件的設計者的設計而造成了不必要的錯誤。

 

在Objective-C裏的物件使用,都是以指標的型態存在,舉個例子來說,在Objective-C 中實作字串的類別是NSString,在程式中宣告一個Objective-C的NSString字串的物件指標,會使用以下語法:

NSString * name = @”iPhone Programming!”;

這裏的name即是一個型態為 NSString 的指標,若在宣告時忘記指定name是一個指標,也就是沒有寫「*」,整個句子變成:

NSString name = @”iPhone Programming!”

 這時編譯(compile)將會出現錯誤。

因為Objective-C裏的物件都是以指標的型態在使用,所以在爾後的章節中提到「Objective-C物件」或是「Objective-C物件指標」時,他們指的是同一件事。

 

2.5.2  id

在C/C++裏有一種特殊的指標:

void *

「void *」在C/C++中稱為泛型指標或是萬用指標。「void *」可以指向任何類型的資料,也就是說不管是「整數指標」、「浮點數指標」等等,他們都可以轉型成「void 指標」。

Objective-C 也定義了一件特別的型態來表示一個物件(物件指標),這個型態叫做:

id

id很像是C語言裏面的「void *」,但是「id」將資料的範圍侷限在指向的目標必須是一個Objective-C的物件,所以我們可以把「id」直接想像成是「物件的指標」,也就是說,所有的Objective-C的物件,都可以轉型成id這個型態 。

註:id的宣告如下

typedef struct objc_object {
     Class isa;
} *id;

在這個宣告中的 Class 是一個pointer. 它的定義如下

typedef struct objc_class *Class;

所以通常isa這個變數會稱為isa pointer。在實務上isa會被用來協助使用者判斷一個id物件是屬於那一種類別(class)。

 

下面是一個id的例子:

id anObject;

上面的anObject即是一個指向物件的指標,他可能是一個Objective-C的字串的物件:「NSString *」的實體(instance),也可能是一個表示日期的物件:「NSDate *」的實體。

若一個id沒有指向任何物件,我們將他的值設成nil。

我們使用

anObject = nil;

來表示anObject目前沒有指向任何一個物件。

註:nil的定義是0。

 

註:id 常見於method(方法)的參數中,目的是把發送訊息的物件,傳送到接收端,這樣就能提供在接收端的method一個存取來源物件或是額外資訊的一個方法。因為許多method在設計之初未必會知道物件的型態是什麼,所以就直接使用id來告訴後來的人,這個參數將會是一個object(物件),然後在method中將id轉型回原來object的型態後再進行相關的操作。

 

2.5.3 物件的記憶體管理

在Objective-C關於物件的記憶體管理方面採用了參考記數(reference counting)的觀念,簡單地來說每個物件自己會有一個變數來紀錄目前有多少人參考它,我們稱這個變數為reference count或是retain count,若有一個物件參考它,則reference count為1,若有2個物件參考它則為2,依此類推。Objective-C所有的物件皆提供了三種方法來提供修改refernce count的方法:

方法

說明

retain

當呼叫一個物件的retain時,這個物件會將自己的refernce count 加1。

release

當呼叫一個物件的release時,這個物件會將自己的refernce count 減1。

autorelease

當呼叫一個物件的autorelease時,這個物件會自未來的某一段時間才會將自己的reference count減1,autorelease和另一個Objective-C的物件autorelease pool相關,我們將在爾後的章節對autorelease pool做進一步的介紹。

在程式的運作上,當A物件要參考B物件時,必須告訴B物件它要使用它,而且在不需要參考B物件時也必須通知它,因此當A物件要參考B物件時「應該」要有以下事情發生:

  1. A物件必須通知B物件,請B物件將自己的reference count 加1。這個增加reference count的動作,在Objective-C中稱作「retain」。在實作上retain這個動作會呼叫B物件的「retain」方法
  2. 而當A物件已經確定不再參考B物件時,則必須請B物件將它的的reference count減1,這個動作稱為「release」。在實作上release這個動作會呼叫B物件的「release」方法
  3. 當B物件的reference count為0時就會呼叫自己的解構式,釋放出佔用的系統資源。

 

註:上面的步驟必須由設計程式的人員自己控管,若上面幾個步驟沒有嚴格遵照Objective-C的記憶體管理原則的話,系統就會發生記憶體相關的錯誤。(memory leak 或 bad access)

 

註:在其他作業系統上常見的garbage collection在iPhone上尚未支援。

 

Objective-C中在reference count的管理上也引用了一種叫做autorelease的觀念,簡單地來說我們呼叫了一個物件的autorelease之後,系統就會在離目前最近的autorelease pool中放入一個當前物件的指標,此時雖然物件本身的reference count不會變,但是當autorelease pool需要清空的時候,它會呼叫所有在pool中物件的release來讓這些物件的reference count減1。

 

註:
Autorelease可視為告訴系統當我們離開目前這個method的範圍後就不再需要這個object(物件)的資源了,請系統在未來的某一段時間,自動將物件的reference count減1。

 

註:autorelease pool可以想像成是一個專門放置即將被呼叫release方法物件的池子。當我們呼一個物件的autorelease 方法之後,系統就會將這個物件放置一份複本在這個池子中,當autorelease pool要消失時,它會呼叫目前在它池子裏所有物件的release方法。這時這些物件的reference count就會減1。

在程式的架構中,autorelease可以是巢狀的架構,每個autorelease pool只會呼叫目前池子裏的物件release方法一次。同一個物件可能在整個autorelease pool巢狀架構中的每一個autorelease pool都有一份複本。

 

註:
Reference counting 在整個 Objective-C 中是非常重要地觀念,有效地掌握reference counting的細節是維護整個程式穩定度的根本,在爾後的章節中我們會更深入介紹在Objective-C中reference counting的運作模式。

 

2.5.4 訊息傳遞 Object Messaging

Objective-C 裏面的訊息(message) 非常類似所謂的函數(function)或是方法(method),但訊息更像是一個動詞,通常我們會說「送一個訊息給一個物件來通知它去執行某個方法。而不是某個物件在執行一個訊息,在Objective-C中,訊息使用方括號來表示。

以下就是一個訊息的例子:

[receiver doSomething]

這裏面的receiver 是一個物件,而doSomething則是告訴這個物件它該做什麼事。在程式碼中,訊息包含了:

  1. 方法的名字。
  2. 傳入方法的參數。

當發送給一個訊息給物件的時候,系統(runtime system)會即時向物件查詢是否有支援該方法然後執行。

 

註: message(訊息)以一般程式設計來說,它的行為類似執行一個方法。在執行一個方法時我們需要指定要用什麼「方法」和這個方法使用的「參數」,通常方法的檢查會在程式編譯的階段執行,但是在Objective-C必須在執行階段才知道這個方法能不能執行。若在Objective-C程式碼中呼叫一個不存在的方法,在編譯階段只會產生警告而不會產生錯誤。

 

方法的參數是由「:」  來引入。方法的參數型態和名稱列在「:」之後,而在「:」之前的字串這個稱為「關鍵字」(keyword)。一個 「:」表示這個方法表示有一個參數,二個「:」  則表示這個 方法有二個參數,依此類推。

下面是一個方法宣告的例子:

 

接下來是一個實際呼叫的例子:

[myPicture setOriginX:30.0  Y:50.0];

在這個例子中myPicture 是一個物件,「setOriginX」和「Y」則是參數的「關鍵字」。上面這個例子的宣告方式為

- (bool) setOriginX: (float)posx  Y:(float)posy;

在整個宣告中,由「:」的個數得知這個方法有會有兩個參數而這兩個參數的型態都是float。這個方法會回傳一個bool。前面有提到 「:」前面的是「關鍵字」,所以這個方法即使不使用「關鍵字」也能達到同樣的功能。例如我們可以宣告另一個方法叫做

- (bool) setOrigin(float)x   (float)y;

這個方法和前一個宣告非常地像也同樣有兩個型態是float的參數,唯一不同的地方就是他們擁有不用的「關鍵字」。

在 Objective-C中,定義了一個名詞「selector」用來表示方法的名字, 「selector」的定義是由:

「關鍵字」+ 「:」

所組成,所以上面兩個方法的「selector」為

setOriginX:Y:

setOrigin::

因此,雖然他們看起來非常相似,甚至可以提供相同的功能。但是因為它們的名字不一樣,所以我們得將他們視為兩個不同的方法。以下的方法雖然參數型態和回傳值都不同,但是在Objective-C裏這些方法的selector都是一樣的。

-(void) SetParam1:(int)param1 Param2:(int)param2;
-(int)  SetParam1:(float)param1 Param2:(int)param2;
-(float) SetParam1:(double)param1 Param2:(double)param2;

以上的3個例子,「selector」都是

 SetParam1:Param2:

 因此在編譯(compile)時會發生錯誤,所以在宣告方法時,必須要小心不要有類似的錯誤發生。

由上面例子中可知Objective-C中方法宣告和使用的方式和C語言相差非常多,但是習慣之後只要在方法命名時稍微用心一點,為「關鍵字」取個容易理解的詞,對程式的可讀性就可以相當大的幫助。

 

註: 在Objective-C中方法的名字稱為「selector」。方法的名字和平常看到的「函數名稱」意思雷同,但是Objective-C提供在程式執行時即時檢查selector是否存在的能力。

 

除了以上簡單的範例之外,方法裏的參數也允許一個以上的輸入,這些額外的參數將用逗號隔開而且不算是selector的一部分。例如下面就是一個帶有多個參數的例子。

[receiver makeGroupgroupmemberOnemembertwomemberThree];

訊息也允許巢狀的結構存在。例如:

[newRect setColor[oldRect getColor]];

這個例子裏,我們可以看到新區域的顏色,是先由舊區域中取出來,再指定到新的位置去。

在C語言中,若存取一個空指標會造成存取違規,但是在Objective-C中,訊息可以送給空的物件指標,以上例來說即是

newRect = nil

而且所有傳送給空物件的訊息都會回傳nil值。以下是一個將訊息送給 nil 時的例子:

id aNilObjectMayNil = nil;
if( [aNilObjectMayNil  methodWillSendtoNilObject] == nil)
{
     //關於物件是nil時該做的事
}

 

註:傳送給message(訊息)給nil時,若原method(方法)回傳的值不是物件或是基本型態(整數、浮點數等等)而是struct(結構)或是vector(向量) 之類的資料結構時,method的回傳值會異常,因此在程式撰寫的過程中應該避免這種寫法出現。

 

2.5.5 類別 (Classes)

物件導向程式的設計通常由一堆物件所組成。而以 Cocoa touch frameworks 為基礎所開發的程式可能會使用NSObject,NSWindow,NSMatrix或是NSDictionary,NSFont,NSArray 等類別。在Objective-C中,我們藉由類別來定義一個物件。類別是一個物件的原型(prototype),而類別中定義了物件有哪些變數和方法可以使用。

當我們想要使用一個物件的時候,我們必須建立一個類別的實體(instance)。所有由同一個類別產生的物件都會共用同樣的一組方法(method)。不過每個物件都擁有自己的會實體變數(instance variables)。編譯器只會為類別產生一個唯一的類別物件(class object)。而這個類別物件能夠知道該如何產生這個類別的實體,所以類別物件也稱為「生產工廠物件」(factory object)或是「生產工廠」。接下來產生的實體都是由「生產工廠」負責產生出來的。

一般來說,類別的命名由大寫英文字母開頭 ,例如: Rectangle,而變數的命名則由小寫英文字母開頭,例如:myRectangle。

 

2.5.6 類別的繼承

類別具有繼承的特性,但是在Objective-C中,每一個類別只能繼承自一個類別,類別會在宣告中說明它是由哪一個類別繼承而來。

當我們在描述繼承關係時,若Class A繼承自 Class B,則稱 Class B 為Class A的父類別(或稱parent class、superclass),反之,Class A 為 Class B 的子類別(或稱child class、subclass)。

 

註:子類別將會繼承其父類別所有的方法和實體變數。若實作的類別不想讓某些實體變數被子類別所繼承,則必須將這些變數的屬性設為私有的(private)。

 

註:實體變數這個詞指的是「在類別中宣告而在實體中可以使用的變數」,實體變數在每個實體都會有一份新的複本。也就是說若「物件 A」和「物件 B」都是「類別 C」的實體。那麼「物件 A」和「物件 B」都會有名稱相同的實體變數但是對各自的實體變數修改時不會影響到其他的物件,也就是說當我們修改「物件 A」的變數時,不會影響到「物件 B」的同名變數,反之修改「物件 B」的變數也不會影響到「物件 A」的同名變數。請參考下面的示意圖:

 

 

 

在實作一個新類別的時候,我們只需針對新功能進行實作即可。不必重覆宣告和實作父類別已擁有的變數和方法。而一個位於繼承樹頂端的類別,我們稱之為根類別(root class)。整個繼承樹中,除了根類別外,每個 類別都必須要有父類別來宣告它是由何處繼承而來。以下圖示了一個繼承樹的範例:

 

在 Foundation framework中,所有類別皆繼承自 NSObject,因此NSObject是整個Foundation framework中唯一沒有父類別的類別。NSObject提供了整個framework裏所有的物件需俱備的基本功能。而裏面兩個最重要的功能如下:

  1. 提供一個變數讓別人詢問這是一個什麼類別。
  2. 實作reference counting相關的操作。


2.6 如何定義一個 類別

Objective-C中,類別 由兩個部分所組成:

l   介面(Interface):定義類別的方法和變數,同時也定義了它的父類別。

l   實作(Implementation):實作類別的功能。

 

註:我們在標頭檔(.h檔)中宣告類別的介面,在實作檔(.m/.mm檔)中進行類別的實作。

 

2.6.1 介面(Class Interface)

介面的宣告由 @interface 開始,由@end結束。在這兩個關鍵字中間可能存在了下面幾種元素:

l   類別的名類(必要條件),類別繼承的父類別和實作的協定。

l   變數的宣告。

l   特性的宣告。

l   方法的宣告。

下面是一個類別的例子:

 

 

以下是類別宣告的定義

@interface ClassName Superclass<protocol1protocol2>
{
     Instance variable declarations //變數宣告。
}
property declarations;                      // 特性宣告
method declarations;                        //方法宣告
@end

 

在第一行裏使用了以下元素:

元素

名字

註解

類別的名稱

ClassName

這個類別的名字。

父類別

Superclass

父類別決定了這個類別在整個繼承樹的位置。它可以是NSObject或是任何由它衍生而來的類別。

實作的協定(protocol)

protocol1、protocol2、protocol3

協定告訴我們這個類別將會提供哪些協定的實作。

 

 

註:

協定類似C++語言中的interface(介面) 概念,但是在Objective-C中的協定除了可以定義實作的類別必須包含哪些method(方法)之外,還能夠定義它所擁有的Property(特性)。因此我們可以說協定除了能夠要求實作的人要做什麼事(method)之外,還能夠要求實作的人必須擁有什麼專有的特性(property)。

 

在接下來的括號中,我們將在此處宣告這個類別所擁有的變數。舉個例來說,如果我們想要為Rectangle 定義它所擁有的變數,下面就是變數宣告的例子:

float width;
float height;
BOOL filled;
int colorR;
int colorG;
int colorB;

這些變數用來紀錄Rectangle 的一些資訊。宣告完變數之後,括號結束接著宣告這個類別要提供的方法(method)和特性(property)有哪些。

特性為類別提供了一個給外面元件存取它內部變數的一個方法。當類別宣告特性之後,系統會自動為特性製作相關的存取方法。這些方法稱為特性的存取方法(accessor method)。存取方法分成下面兩類:

setter

提供設定特性的功能

getter

提供了讀取特性的功能

setter在一般的程式中就是類似 setVariable(),而getter則是 類似getVariable() 的方法。不過這些setter和getter方法並不需要我們自己實作,只要對特性進行適當地設定,系統就會自動幫我們合成這些實作的內容。

特性可以經由設定使它適合在多執行緒的環境使用,因此妥善的特性規劃可以簡化程式的設計,下面是幾種特性的宣告方式,在這一節中暫時不做說明,關於特性的設定我們將在後面的章節做更進一步的介紹。

@property (nonatomicretain) UIView * testView;
@property (atomic) int fileCount;
@property (nonatomicassign) UIMyDelegete * delegate;
@property (nonatomicassign) UIView * parentView;

 

設定完特性之後,接下來可以為類別決定該提供哪些方法。方法分為以下兩類

辨識符號

方法的型態

說明

+

類別方法

(class method)

提供給類別的方法,在使用時會直接對 類別物件進行操作。

-

實體方法

(instance method)

提供給實體物作的方法,在使用時必須對實體物件進行操作。

 

下面是方法的宣告的一個例子:

 

類別方法由「+」開始,以下是NSString中一個類別方法的例子:

+ (id)stringWithFormat(NSString *)format...

在程式中,我們可以利用這個類別方法來產生一個NSString物件。就像是下面的例子:

int nStringNumber = 1;
NSString * myFirstString = [NSString stringWithFormat@”This is string %d”nStringNumber];

以上的程式,myFirstString 的值將會是「This is string 1」。

 

註:在上面的例子中,因為myFirstString是透過class method (類別方法)所產生的,在Objective-C的慣例中,由class method產生的物件將會自動放入autorelease pool中。若class method並非用來產生物件,而只是進行一些資料的存取或計算,則回傳的物件就不一定會放至autorelease pool 中。在實作上,若是該放入autorelease pool的物件沒有放入autorelease pool中會造成許多不必要的錯誤。因此在實作 method(class method或是 instance method)時必須小心地處理記憶體管理相關的細節。Autorelease pool 是objective-C負責處理物件記憶體的一個物件。在後面關於記憶體管理的章節將再為auto release pool的概念做進一步的介紹。

 

實體方法由「-」開始,下面的例子是一個NSString 的實體方法

- (NSString *)substringFromIndex(NSUInteger)anIndex

這個實體方法為NSString的物件提供了一個取得子字串的方法。在程式中可以如此使用:

NSString * sourceString = @”1234567890”;
NSString * substring = [sourceString substringFromIndex3];

 

註:在實體方法和類別方法比較之後,我們可以發現,類別方法在角括號中使用的是類別的名稱: NSString,而在實體方法中,我們使用的是實體: sourceString;至於何時該使用類別方法和何時該使用實體方法完全視情況而定。這兩種方法並沒有優劣的關係存在。

 

2.6.2  滙入介面(importing interface)

當需要使用到一個類別時,我們必須滙入這個類別的宣告,也就是標頭檔(header file, 也就是.h檔)。在Objective-C中使用#import來滙入標頭檔。舉個例子來說,當ClassA 繼承自ClassB,在ClassA的標頭檔就必須引入包含ClassB的標頭檔。如以下所示:

#import “ClassB.h”
@interface ClassA ClassB
{
     instance variables
}
methods
@end

當類別有父類別時,我們必須使用#import滙入父類別的標頭檔,#import會將父類別的所有宣告同時滙入,因此滙入可視為將兩個標頭檔合而為一的一種方式。在這種結構下,滙入父類別也暗示了滙入整個類別的繼承樹,因此只要滙入父類別的標頭檔後,整個繼承樹的類別都可以在目前的類別中使用。

 

2.6.3 引用其他類別

通常我們會在下面情況下被要求要將類別的標頭檔滙入:

  1. 宣告一個擁有父類別的類別時,必須滙入父類別的標頭檔。
  2. 使用到一個類別,但是這個類別不在目前的繼承樹裏時,必須滙入該類別的標頭檔。

之所以要滙入標頭檔是因為編譯器必須在滙入類別的標頭檔後才能知道這些類別的定義是什麼,該如何為它們配置記憶體等等相關的資訊。

但是若我們僅是使用到類別的「指標」,因為指標本身所代表的意義是記憶體的位址,它的記憶體配置方式和所指向的物件型態沒有關係,所以這時則除了滙入該類別的標頭檔外,亦可以使用 @class 來告訴編譯器我們即將引用這個類別,但是沒有打算匯入該類別的定義,例如:

@class RectangleCircle;

以上的宣告是告訴編譯器我們將會使用到Rectangle和 Circle這兩個類別,但是因為我們暫時不需要它們實際宣告的內容,所以我們不打算將它們的標頭檔滙入。

在我們使用以上宣告後,編譯器會將這些類別視為一般的變數型態,不會要求我們必須滙入這些類別的宣告。

下面的方法宣告的例子就是告訴編譯器,我們使用了一個 Rectangle類別的指標:

-(void) setRectangle(Rectangle *) rect;

在一般的情況下,因為Rectangle不是內建的型別,所以當我們要使用它時,編譯器會要求我們必須滙入Rectangle的定義,否則它會不知道Rectangle是什麼東西,但是在這個方法的宣告中,rect這個變數是一個Rectangle指標,在這裏它的目的是用來傳遞某一個Rectangle物件的記憶體位址,因此我們要的只是一個位址的資訊而已,即使不了解Rectangle的內容對整個程式也沒有影響,在這種情況下,若我們想要使用Rectangle這個類別,利用滙入或是引用的方式都可以達到我們的需求。

 

註:若使用@class方式來帶入Rectangle時,雖然目前不須將整個類別的定義滙入,但在實作時(.m檔)如果需要用到Rectangle的內容(例如存取成員變數),因為此時必須存取實體的內容,所以仍需將Rectangle的檔頭檔滙入。

 

 

2.6.4 介面扮演的角色(The Role of the Interface)

介面檔(.h檔)的目的是為了宣告一個類別的內容來讓別人使用。它包含了關於和這個類別進行互動所需要的任何資訊。以下是介面在整個程式架構中所扮演角色的一個摘要:

-          介面檔提供使用者對於整個繼承樹的連入點。它提供別人存取整個繼承樹類別或是加入繼承樹的方法: 繼承該類別或是將該類別做為一個變數來存取。

-          介面檔提供編譯器足夠的資訊,讓編譯器知道這個類別的物件含有什麼實體變數(instance variables)可以使用,什麼變數能夠讓它的子類別使用。

-          介面提供方法(methods)的宣告,讓其他使用者知道該傳送何種訊息給這個類別的類別物件(class object)和實體物件 (instance object)。


2.7 類別的實作(Implementation)

類別的實作由 @implementation 開始,由@end 結束,除了實作本身的內容外,我們也須將類別的宣告滙入,下面是實作檔的格式:

#import “ClassName.h”             //滙入類別的宣告。
@ implementation ClassName  //說明這個實作類別的名稱。
@synthesize property               //告訴編譯器有哪些特性需要合成。
method definitions                    //方法的實作
@end                                       //結束類別的實作。

方法的實作方式類似方法的宣告,用大括號將實作的內容包起來

+(id) alloc
{
     …
}

-(BOOL)isFilled
{
     …
}

-(BOOL)setFilled(BOOL)flag
{
     …
}

 

2.7.1 合成特性(synthesize property

在類別中宣告的特性,在實作檔中必須使用@synthesize來要求編譯器產生這些特性的實作。因此當介面檔宣告了以下的特性

@property (nonatomicretain) UIView * testView;
@property (atomic) int fileCount;
@property (nonatomicassign) UIMyDelegete * delegate;
@property (nonatomicassign) UIView * parentView;

在實作檔中必須使用

@synthesize testViewfileCountdelegateparentView;

來通知編譯器將這些特性實作出來。

 

2.7.2 存取實體變數(accessing instance variables

在方法中存取自己的實體變數只要直接使用變數的名稱即可。舉個例來說,如果目前的實體有一個叫做 filled 的實體變數。下面是一個存取這個變數的例子:

-(void) setFilled(BOOL)flag
{
     filled = flag;
}

但若是在方法中要存取別的實體的實體變數,則必須明確地指定實體的值,就像是下面這個例子中我們將在makeIdenticalTwin中設定其中一個變數:

@interface Sibling NSObject
{
     Sibling * twin;
     int gender;
     struct feature * appearance;
}

-(void)makeIdenticalTwin;
@end

下面是makeIdenticalTwin的實作:

-(void)makeIdenticalTwin
{
     if(!twin)
     {
           twin = [[[Sibling alloc] init];
           twin->gender=gender;
           twin->appearance=appearance;
           … //其他關於twin的操作.
     }
     return;
}

 

註: 「->」運算子是一種專屬於物件指標的一種運算子。這個運算子的功能是提供使用者存取物件成員的一個方法。以上例來說「twin」本身是一個物件指標,因此可以使用「->」運算子來存取它的genderappearance這兩個成員變數。

 

 

2.7.3 變數的適用範圍(The scope of Instance Variables)

一般來說在設計一個類別的時候有三種變數的適用範圍可供我們在定義變數時使用,這三個範圍分別為private、protected和public。下面是這三種範圍的摘要介紹:

指令

說明

@private

只允許在「宣告它們的類別」中存取。

@protected

允許在「宣告它們的類別或是繼承它的類別」中存取。

@public

允許所有人存取。

 

舉個例子來說如果我們要設定一個類別來管理某人的財產,我們可以將這個人的財產分成三類,private顧名思義就是這個人的私房錢,沒打算給別人用的,不管是任何人都不知道這人的私房錢到底有多少,而protected就像是要留給子孫的,他的子孫們都可以拿來用,public就是這個人拿來做公益捐出來的,任何人都能夠拿來使用。

 

註:在目前的iPhone系統中有另一種型態的變數範圍稱為@package,通常在製作framework 時使用,這種類型的變數在同一framework中和@public相同,但對framework 外的class則視同@private。

 

下面以圖示來說明上面的表格:

 

 

下面是一個範例:

@interface Worker NSObject
{
     char *name;
@private
     int age;
     char *evaluation;
@protected
     id job;
     float wage;
@public
     id boss;
}

在預設的情下,所有沒有標示範圍的變數(如上面的name變數)皆為protected。

 

2.7.4 傳送訊息給selfsuper(Messages to self and super)

Objective-C 提供了兩個相當實用的隱藏「指標變數」給自己的方法使用:self和super。self可以完全視為一個指向目前實體的實體變數。所有可以對目前實體進行的工作都可以透過self來達成。super則僅能做為訊息的接收者(receiver)。兩者的差別在於對收到訊息時的搜尋起始點不同。以下是這兩者在收到訊息時的動作:

self

由目前的實體開始搜尋。

super

直接由目前定義這個方法的類別的父類別開始搜尋。

底下我們用程式碼來檢視一下這兩個變數搜尋的流程:

首先,我們先定義High、Mid、Low三個interface:

@interface High NSObject
- (void) negotiate;
@end

@interface Mid High
- (void) negotiate;
- (void) SelfTesting;
- (void) SuperTesting;
@end

@interface Low Mid
- (void) negotiate;
@end

接下來是這三個介面的實作,我們在這些方法中印出它們執行的過程:

@implementation High
- (void) negotiate
{
     NSLog (@"This is negotiate from High!!");
}
@end

@implementation Mid
- (void) negotiate
{
     NSLog (@"This is negotiate from Mid!!");
}

- (void) SuperTesting
{
     [super negotiate]; 
     NSLog (@"This is SuperTesting from Mid!!");
}

- (void) SelfTesting
{
     [self negotiate];
     NSLog (@"This is SelfTesting from Mid!!");
}
@end

@implementation Low
- (void) negotiate
{
     NSLog (@"This is negotiate from Low!!");
}
@end

讓我們用程式碼檢視一下使用上面的三個類別會有什麼結果:

首先讓我們對目前的實體呼叫SelfTesting方法

01 Low * test = [[[Low alloc] init] autorelease];
02 NSLog (@"Start self test ");
03 [test SelfTesting];    

在上面的第三行(編號03)中,我們由目前的類別呼叫只存在於它的父類別才有的方法:SelfTesting。這個訊息會進行以下動作:

  1. 去現在的實體中尋找SelfTesting這個方法。
  2. 因為找不到,所以它繼續往它的父類別尋找這個方法。
  3. 在父類別找到這個方法之後,這個方法會執行:[self negotiate]。
  4. 再從目前的實體中尋找到negotiate這個方法。

所以我們得到了下面的結果:

Start self test
This is negotiate from Low!!
This is SelfTesting from Mid!!

接下來,我們試著呼叫SuperTesting來看看會發生什麼事:

04 NSLog (@"Start super test ");
05 [test SuperTesting];

在第五行(編號05中,一樣尋找的流程,但是這次找的方法是SuperTesting,在這的方法中會執行:[super negotiate],這時搜尋negotiate的目標將不是由目前的實體開始,而是由「定義這個方法的父類別」,也就是 High 這個類別開始搜尋。最後我們執行了High這個類別的negotiate method,所以得到了下面的結果:

Start super test
This is negotiate from High!!
This is SuperTesting from Mid!!

 

在上面的例子中,若想要使用Mid的negotiate就必須直接建立Mid這個類別的實體然後呼叫它。

由以上的例子可以了解到self和super的使用方式。這兩個隱藏變數在程式中扮演了相當重要的角色,在後面的章節我們將會常常和這兩個變數見面。

 


 

2.8物件的配置及初始化

2.8.1物件的配置及初始化

Objective-C裏建立一個物件有兩個步驟:

l   動態配置記憶體給的新的物件。

l   呼叫適當的物件初始方法,初始化物件。

當上面兩個步驟執行完成後,物件才能夠正常地使用。下面是一個建立物件常見的範例:

id newObject = [[MyCustomClass alloc] init];

上面這個式子可以分成兩個部分來看:

  1. [MyCustomClass alloc] 負責配置記憶體給新的物件。
  2. [object init] 呼叫物件的init初始化方法。

第二式中的object就是在第一個式子中配置得來的物件。在第一式中的alloc會向系統要求記憶體來給即將產生的實體使用。

若物件提供不同的初始化方法,亦可視狀況來決定要呼叫哪一個,可能是init,也可以是initWithSomething等專屬於該物件的初始化方法

 

2.8.2 合併物件的記憶體配置和初始化

除了屬於實體的初始化方法之外,許多類別也提供了類別的初始化方法,類別的初始化方法將記憶體的配置和物件的初始化合併在一個步驟中完成。如下面就是一個NSString 類別初始方法

+ (id)stringWithString:(NSString *)aString

在實務上,可以使用上面這個方法會產生一個在autorelease poolNSString物件。如:

NSString * myString = [NSString stringWithString:@”This is a autorelease string”];

執行完後myString的值為This is a autorelease string,但是myString物件因為是由類別方法產生的,因此會有一份副本存在autorelease pool中,我們只能在目前的方法中安全地使用它。

以下是上面字串乎叫retain的方法:

[myString retain];

 

註:類別和實體初始化方法不同的地方是:實體的初始化方法作用的對象必須是一個實體,而類別方法的對象則是類別的名稱,換句話說在使用實體的初始化方法之前一定會有一個alloc的過程來負責配置實體所需的記憶體。

一般來說類別的初始化方法會直接回傳一個已經在autorelease pool的實體。不過也有一些類別初始方法回傳的是一個共用的實體變數,因此關於類別方法回傳的物件的記憶體管理有時會需要參考其他的文件能才能知其內部正確運作方式。

 

註:為什麼autorelease pool 裏的東西只能保証在目前的方法內能正常使用呢?

 因為autorelease pool 可能會在離開目前方法後的任何時間呼叫myStringrelease方法,這時myStringretain count 會被減1,若此時myStringretain count變成0的話,myString就會被系統釋放,後面如果有其他人要再次存取這個變數就會造成存取違規,因此我們必須呼叫myString retain來幫myStringretain count 1,以確保myString不會在一離開目前的方法範圍retain count變成0,被系統釋放掉。

 

2.8.3實作初始化方法(Initializer)

要實作初始化方法時必須參考下列幾個準則:

  1. 初始化方法必須由init開頭。例如:initWithFormat:initWithObjects:
  2. 初始化方法的回傳值型態必須為id
  3. 若要設定自訂的初始化方法,在方法中必須呼叫上層類別中的預設初始方法。大部分的類別的預設初始方法為:init。如:NSObject
  4. 必須將上層初始方法的回傳值指定給self
  5. 設定類別內部變數的初始值。通在初始化方法中會直接對變數做設定而不會透過存取方法(accessor)來設定以避免一些不必要的狀況發生。例如:在實作accessor時也許可能會參考到其他變數,而這個變數還沒被設定過,隨意存取可能會造成存取違規。
  6. 在初始化方法結尾時,一切正常則回傳self,若有異常則回傳nil

下面是一個常見的初始化方法樣式:

-(id) init {
     if(self = [super init]) { //注意,這一行是指定,而不是比較。
           myVariable1 = 1;
           myVariable2 = 2;
           myVariable3 = 3;

          if(something error)
                   {
                           [self release];
                           return nil;
           }
     }
     return self;
}

 

在上面的例子中

self = [super init]

這種寫法是在Objective-C中初始化方法常用的一種錯誤控管機制,因為我們無法保証初始化方法的結果是一定成功,因此在初始化方法中若遇到了錯誤就應該要呼叫[self release]並傳回nil。而上面這一行可以在我們在呼叫父類別的初始化方法時遇到錯誤時直接回傳nil而不必進行額外的初始化工作。(記得我們在之前曾說明nil的值為0嗎?故self=0會使得if條件值為0而直接跳至return self然後離開初始化方法。)

 

2.9 協定(Protocol)

1.9.1         什麼是協定?

協定類似C++裏的interface,基本上可視為是許多方法宣告的組合。

 

以下是幾個和滑鼠回應相關的方法:

- (void)mouseDown:(NSEvent *)theEvent;
- (void)mouseDragged:(NSEvent *)theEvent;
- (void)mouseUp:(NSEvent *)theEvent;

 

任何一個類別若想要提供和滑鼠動作相關的反應就可以實作上面這些方法。我們用一個協定 MouseMoveResponse來將這些方法包起來,當別人看到某個類別有實作MouseMoveResponse這個協定時,就會知道這個類別有提供一些對滑鼠反應的功能。

 

2.9.2 協定的宣告(Formal Protocols)

我們使用@protocol來宣告協定並以@end來結束協定的宣告,以下面是在iPhone開發裏最常看到的協定 NSObject的定義:(僅截取部分做為範例)

@protocol NSObject
- (BOOL)isEqual:(id)object;
- (Class)superclass;
- (Class)class;
- (id)self;
- (NSZone *)zone;
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
- (id)retain;
- (oneway void)release;
- (id)autorelease;
- (NSUInteger)retainCount;
- (NSString *)description;
@end

 

因為類別 NSObject實作NSObject協定,因此可以直接由這個協定得知所有繼承自NSObject的類別至少都有提供以上協定內定義的功能。

 

2.9.3 Optional protocol methods

在協定中可以宣告某些方法為選用的方法,不一定要實作,這些方法將會以@optional開始,而使用了@optional之後若要再次定義必須實作的方法時就必須使用@required來隔開,在預設的情況下沒有標記的方法都是required,例如下面的例子:

@protocol MyProtocol
- (void)requiredMethod;            //需實作
@optional
- (void)anOptionalMethod;                                  //不需實作
- (void)anotherOptionalMethod;                          //不需實作
@required
- (void)anotherRequiredMethod;                         //需實作
@end

 

底下是另一個常見的協定 UITextFieldDelegate的宣告(僅截取部分做為範例):

@protocol UITextFieldDelegate <NSObject>
@optional
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;
- (void)textFieldDidBeginEditing:(UITextField *)textField; 
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;
- (void)textFieldDidEndEditing:(UITextField *)textField; 
- (BOOL)textFieldShouldClear:(UITextField *)textField;
- (BOOL)textFieldShouldReturn:(UITextField *)textField;
@end

 

這個協定中所有的方法都是optional的,在iPhone SDK中委派主要做為訊息通知使用,因此實作此類協定的類別沒有必要為所有的訊息做處理,我們通常可以在以delegate為結尾的協定中看到許多optional的方法。

 

iPhone SDK中可以見到許多協定的使用範例,舉個例來說:許多iPhone SDK中的物件都會伴隨著一個委派的宣告,而這個委派就是協定的一個應用實例。因為委派在iPhone SDK中幾乎可說是隨處可見,因此我們有必須先了解一下委派到底是什麼?

 

一般來說,委派(delegate)的目的是為了:

 

  1. 讓某個物件能夠將它的一些狀態通知委派。
  2. 委派收到通知後依據目前的狀態執行某些工作。

 

舉個例子來說,UITextField這個類別提供了一個讓使用者輸入文字的介面,此外text field也伴隨了一個叫做UITextFieldDelegate的協定來定義一些和text field相關的行為,例如textFieldDidBeginEditing:這個方法表示使用者即將要開始輸入文字。

 

當使用一個text field物件而且希望有機會能在使用者開始輸入文字前做一些事,我們就可以宣告一個類別並在這個類別(假設這個類別叫做D)」實作UITextFieldDelegate這個協定,然後向text field物件指定D的實體是他的委派,這樣的話,當使用者要開始輸入文字的時候,text field就會通知D的實體使用者即將要輸入文字,讓D的實體有機會在使用者輸入資料前執行一些動作。

 

註 :
當程式是執行時有所以謂的同步執行和非同步執行,下面是這兩者的介紹:

 

同步執行

當在A方法中呼叫B方法時,A方法必須等待B方法所有的工作都完成以後再繼續執行自己未完成的工作。

非同步執行

當在A方法中呼叫B方法時,A方法不等待B方法的工作完成,馬上執行自己接下來的工作,這時,若我們想要知道B方法的執行結果,就必須要求B方法將它的執行進度回報給某一個方法。

 

 

註:
協定在Objective-C裏常用來做為一個類別的delegate(委派)。當某個物件希望你幫它處理某些事時,它可以透過delegate來通知你來幫它完成或著說你可以讓某個類別將它執行完某個工作時利用delegate的方式將結果傳給你,而你再接手之後的工作繼續進行。這種執行模式稱為非同步執行。

 

待續


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 穿越時空的旅人 的頭像
    穿越時空的旅人

    穿越時空的旅人

    穿越時空的旅人 發表在 痞客邦 留言(1) 人氣()