[Objective-C] 程式設計教學:撰寫類別 (Class) 和物件 (Object)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    除了使用在 Cocoa 或 GNUstep 中已存在的類別外,我們也可以利用 Objective-C 的物件系統建立新的類別。由於 Objective-C 是 C 的延伸,實作類別時仍然會用到 C 的部分,而類別和物件相關的語法則由 Objective-C 所提供。本文以簡單的範例來看如何在 Objective-C 中建立類別。

    Objective-C 程式碼的架構

    除了主程式外,大部分 Objective-C 程式會以物件 (object) 的形式來運行。物件根據類別 (class) 來生成。撰寫類別是撰寫 Objective-C 程式的基礎工作。

    撰寫 Objective-C 類別時,類別的公開界面和內部實作會拆開在兩個檔案。公開界面位於標頭檔中,內部實作放在原始檔中。使用標頭格和原始碼是相容於 C 的設計。在 Objective-C 的撰碼習慣上,標頭檔以 .h 為副檔名,原始檔以 .m 為副檔名,而純 C 的原始檔則繼續以 .c 為副檔名。

    宣告類別的公開界面

    Objective-C 的公開界面的虛擬碼如下:

    @interface Klass : Base {
        /* Instance variable declarations. */
    }
    
    /* Public message declarations. */
    
    @end
    

    @interface@end 是固定的語法,公開界面寫在兩者所包起來的區塊中。

    Klass 代表此類別的名稱,而 Base 代表所繼承的類別名稱。典型的 Objective-C 類別至少會繼承 NSObject 這個共通的基礎類別,完全不繼承任何類別的類別反而少。

    在一對大括號 {} 所包住的區塊是實體變數 (instance variable) 的宣告區塊。其他的部分則是訊息 (message) 的宣告區塊。Objective-C 使用訊息而非主流物件導向程式中的方法 (method) 是因為兩種物件系統在運行期的行為上有差異。

    Objective-C 的訊息可細分為實體訊息 (instance message) 和類別訊息 (class message)。兩者的差別在傳入的物件是實體 (instace) 還是類別 (class)。另外,類別訊息中無法使用實體變數,而實體訊息可以。類別本身視為特殊物件。

    實體訊息的虛擬碼如下:

    -(type) instanceMessageWith: (type_a) a andObj: (type_b) b;
    

    訊息 instanceMessageWith:andObj: 前綴使用減號 - 代表該訊息是實體訊息。本範例訊息接收兩個參數,參數的型態分別是 type_atype_b。該訊息回傳的型態為 type

    類別訊息的虛擬碼如下:

    +(type) classMessageWith: (type_a) a;
    

    訊息 classMessageWith: 前綴使用加號 + 代表該訊息是類別訊息。本範例訊息接收一個參數,該參數型態為 type_a。該訊息的回傳型態為 type

    接下來,我們用實際的範例來展示其寫法。在本文中,我們使用平面座標的點 (point) 當成範例。採用這個例子的原因是點易於實作,可以專心在練習類別和物件的語法上。

    以下是 Point 類別的公開界面:

    /* point.h */
    #pragma once
    
    #import <Foundation/Foundation.h>
    
    @interface Point : NSObject {
        double x;
        double y;
    }
    
    -(Point *) init;
    -(Point *) initWithX: (double) px Y: (double) py;
    +(double) distanceBetween: (Point *) p andPoint: (Point *) q;
    -(double) x;
    -(double) y;
    @end
    

    我們先來看屬性的部分:

    @interface Point : NSObject {
        double x;
        double y;
    }
    

    這個部分告訴編譯器我們的類別是 Point,有兩個屬性 xy。此 Point 屬性繼承 NSObject 類別。Objective-C 採用單一基礎類別,大部分的類別至少會繼承 NSObject 類別,若有需要也可以繼承其他的類別。

    Objective-C 沒有真正為建構子 (constructor) 而設的語法,使用一般的訊息充當建構子即可。本範例程式宣告了兩個建構訊息:

    -(Point *) init;
    -(Point *) initWithX: (double) px Y: (double) py;
    

    第一個訊息用來建立位於原點 (0.0, 0.0)Point 物件。第二個訊息則在建立物件時一併指定該物件所在的位置。

    雖然我們可以用任何名字來當成建構子訊息,一般常用的訊息名稱是 initnew。兩者的差別在於 init 訊息多不配置記憶體,只負責初始化物件,而 new 則會一併配置記憶體和初始化物件。但這不是強制性的,只是習慣,所以還是得閱讀該類別的 API 手冊才知道該訊息實際的行為。

    注意一下我們的建構子訊息是實體訊息,而非類別訊息,這似乎和主流程式語言略為不同。因為 Objective-C 建立物件的敘述如下:

    Point *p = [[Point alloc] init];
    

    在這行敘述中,[Point alloc] 會為 Point 物件配置記憶體,但這時候該物件還未初始化。接著,對配置好的匿名物件傳入 init 訊息,完成初始化的動作。

    假若我們有實作 new 訊息,則建立物件的敘述會變成:

    Point *p = [Point new];
    

    因為 new 在內部偷偷地進行 allocinit 兩個訊息的動作,所以我們只要寫單一訊息即可。

    我們另外用一個類別訊息來計算兩點間的距離:

    +(double) distanceBetween: (Point *) p andPoint: (Point *) q;
    

    該訊息的使用方式如下:

    /* p and q are instances of `Point`. */
    double dist = [Point distanceBetween: p andPoint: q];
    

    類別訊息和實體訊息主要的差別是訊息所傳遞的物件相異,在語法上則雷同。以本範例來說,distanceBetween: andPoint: 是類別訊息,故會傳遞給 Point 類別。

    其實這裡不一定非得用類別訊息來寫,也可以用實體訊息來寫。Objective-C 在這方面相當自由。

    實作類別的內部

    實作部分的虛擬碼如下:

    @implementation Klass
    -(type_1) instanceMessageWith: (type_a) a andObj: (type_b) b {
        /* Implement instance message here. */
    }
    
    +(type_2) classMessageWith: (type_a) a {
        /* Implement class message here. */
    }
    @end
    

    Objective-C 的類別實作放在一對 @implementation@end 所包起來的區塊。 Klass 是該類別的名稱。每個訊息再分別以一對大括號 {} 區隔開來。在公開界面中宣告的訊息,在原始碼中都得有相對應的實作。

    接著,我們分段來看 Point 類別的內部實作。先來看建構子訊息:

    -(Point *) initWithX: (double) px Y: (double) py {
        if (!self)
            return self;
     
        self = [super init];
        x = px;
        y = py;
    
        return self;
    }
    

    為什麼一開始會有這段程式碼呢?

    if (!self)
        return self;
    

    這是因為 Objective-C 採用兩段訊息來建立物件:

    Point *p = [[Point alloc] init];
    

    [Point alloc] 訊息完成後,會配置一個尚未初始化的物件,該物件在建構子訊息中相當於 self,所以我們要檢查 self 本身不為空。

    接著,我們先用父類別的初始化訊息來初始化 self 物件:

    self = [super init];
    

    這是因為 Point 類別繼承自 NSObject 類別,所以要先由父類別初始化後,再由本類別繼續完成初始化的動作。

    以本範例程式來說,我們只要將 xy 的座標值存入物件即可。雖然 Objectiive-C 有 self 指標,但沒有 self->x 這種寫法,所以在命令參數時,要用相異的名稱,像是這裡的 xpx,才能區分內部變數和外部參數。

    接著,我們接著來看求兩點距離的訊息:

    +(double) distanceBetween: (Point *) p andPoint: (Point *) q {
        double dx = [p x] - [q x];
        double dy = [p y] - [q y];
        return sqrt(pow(dx, 2) + pow(dy, 2));
    }
    

    注意到我們在這裡呼叫自己所實作的其他訊息。由於訊息在本質上是一種特化的函式,所以可在訊息實作區塊內呼叫其他訊息。

    另外,我們在求距離時,仍然會用到 C 語言的 pow() 函式及 sqrt() 函式。如同我們前文所述,學 C 是學 Objective-C 的預備知識,因為我們還是有機會用 C 來處理實作類別的細節。

    我們在本節末段列出 Point 類別的範例實作,給讀者參考:

    /* point.m */
    #include <math.h>
    #import "point.h"
    
    @implementation Point
    -(Point *) init {
        if (!self)
            return self;
    
        self = [super init];
        x = 0.0;
        y = 0.0;
    
        return self;
    }
    
    -(Point *) initWithX: (double) px Y: (double) py {
        if (!self)
            return self;
     
        self = [super init];
        x = px;
        y = py;
    
        return self;
    }
    
    +(double) distanceBetween: (Point *) p andPoint: (Point *) q {
        double dx = [p x] - [q x];
        double dy = [p y] - [q y];
        return sqrt(pow(dx, 2) + pow(dy, 2));
    }
    
    -(double) x {
        return x;
    }
    
    -(double) y {
        return y;
    }
    @end
    

    使用 Objective-C 物件的外部程式

    最後,我們以簡短的外部程式來看如何使用 Point 類別:

    /* main.m */
    #include <stdio.h>
    #import "point.h"
    
    int main(void)
    {
        Point *p = nil;
        Point *q = nil;
        
        p = [[Point alloc] init];
        if (!p) {
            perror("Failed to allocate p\n");
            goto ERROR;
        }
    
        q = [[Point alloc] initWithX: 3.0 Y: 4.0];
        if (!q) {
            perror("Failed to allocate q\n");
            goto ERROR;
        }
    
        if (!(5.0 == [Point distanceBetween: p andPoint: q])) {
            perror("Wrong distance\n");
            goto ERROR;
        }
        
        [q release];
        [p release];
    
        return 0;
    
    ERROR:
        if (q)
            [q release];
    
        if (p)
            [p release];
    
        return 1;
    }
    

    在這個範例中,我們用兩種訊息分別建立物件 p 和物件 q,再求其距離。最後釋放掉所配置的記憶體。程式中已經包含錯誤處理的部分。

    細心的讀者可能會注意到我們並沒有為 Point 類別實作 allocrelease 訊息,為什麼還是可以使用呢?因為 Point 類別繼承自 NSObject 類別,而 NSObject 類別已經幫我們實作了物件的基本訊息。

    編譯範例程式

    可參考以下指令來編譯範例程式:

    $ gcc -o point point.m main.m -lm -lobjc -lgnustep-base -I /usr/include/GNUstep -L /usr/lib/GNUstep
    

    這個編譯指令比較長是因為 Objective-C 不是類 Unix 系統的標準語言,GNUstep 也不是類 Unix 系統的標準物件庫,所以要自己指定 GNUstep 所在的路徑。目前的解決方式是改用我們先前提到的 GNUstep Make 來管理 Objective-C 專案。

    結語

    雖然本文所使用的範例相當簡單,但簡單的範例更可以突顯類別和物件相關的語法,不會被實作細節所困住。由於 Objective-C 中和物件相關的語法和主流語言差異較大,建議多讀一些範例或是自己動手寫一些小範例,慢慢體會 Objective-C 的物件系統。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk