[Objective-C] 程式設計教學:不使用 Foundation 物件庫寫 Objective-C 類別

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

    前言

    一般來說,我們在寫 Objective-C 類別時,會繼承 NSObject 這個基礎類別,因為該類別已經實作物件應有的的基本行為,使用者就不用再重新實作一次。

    然而,有時候我們想要用 Objective-C 的物件語法,但不想引入 Foundation 物件庫,這時候其實我們還是可以自行用 Objective-C 的運行期函式庫實作不依賴 NSObject 的類別。本文以簡單的實例來展示如何實作這樣的類別。

    宣告公開介面

    我們仍然以平面座標的點為例,來看如何實作這樣的類別。這個例子由於易於實作,很適合當成練習的題材。而且讀者可和前文相互比較,觀察兩者的差異。

    首先是公開界面的部分:

    /* point.h */
    #pragma once
    
    #pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"
    #pragma clang diagnostic ignored "-Wobjc-root-class"
    
    @interface Point {
        Class isa;
        double x;
        double y;
    }
    
    +(Point *) new;
    +(Point *) newWithX: (double) px Y: (double) py;
    +(double) distanceBetween: (Point *) p and: (Point *) q;
    -(double) x;
    -(double) y;
    -(void) release;
    @end
    

    請注意我們的屬性中多了一個 isa,該變數的型別是 Class。這算是比較舊式的手法,在沒有基礎類別時才會用到。

    由於本範例的手法不是現在的主流,使用 Clang 編譯此範例程式時,Clang 會發出警告訊息。因此,我們使用 Clang 的 pragma 關掉了兩個相關的警告。

    本範例改以兩個類別訊息當成建構子訊息:

    +(Point *) new;
    +(Point *) newWithX: (double) px Y: (double) py;
    

    會修改建構子訊息的原因是本範例程式的 Point 類別沒有繼承 NSObject 類別,原本的 alloc 訊息是無法使用的。我們會在單一訊息中一併完成配置記憶體和初始化物件的動作。

    另外,我們自己實作了 release 訊息:

    -(void) release;
    

    這同樣是因為我們沒有現成的 release 訊息可用,所以得自行實作。

    實作類別內部

    接著來看內部實作的部分:

    /* point.m */
    #include <math.h>
    #include <stdlib.h>
    #import <objc/runtime.h>
    #import "point.h"
    
    @implementation Point
    +(Point *) new {
        Point *p = (Point *) malloc(class_getInstanceSize(self));
        p->x = 0.0;
        p->y = 0.0;
        p->isa = (id) self;
        return p;
    }
    
    +(Point *) newWithX: (double) px Y: (double) py {
        Point *p = (Point *) malloc(class_getInstanceSize(self));
        p->x = px;
        p->y = py;
        p->isa = (id) self;
        return p;
    }
    
    +(double) distanceBetween: (Point *) p and: (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;
    }
    
    -(void) release {
        free(self);
    }
    @end
    

    由於我們沒有繼承 NSObject,像 alloc 等基本的訊息是無法使用的。我們也沒有必要重新實作 alloc,可以回頭用 C 的 malloc() 函式來配置記憶體。但是要記得把 isaself 賦值,Objective-C 的運行期動態行為才能正常運作。

    由於我們使用了 malloc() 函式,最後要釋放記憶體時也要用成對的 free() 函式。

    由於我們要和 MacOS 上的 Clang 相容,我們使用了 objc/runtime.hclass_getInstanceSize() 來計算物件寛度。如果不需考慮相容性的話,我們甚至可以用 sizeof 來取代原本的敘述:

    Point *p = (Point *) malloc(sizeof(Point));
    

    這是因為在舊版的 Objective-C 中,Objective-C 類別內部其實直接對應到結構體,所以可以用 sizeof 來計算寬度。這時候標頭檔 objc/runtime.h 也可以省略了。

    使用 Objective-C 風格的物件

    我們以實例來看如何使用這樣的類別:

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

    基本上和一般的 Objective-C 程式沒什麼差別。

    編譯本範例程式

    如果想使用 GCC 編譯此程式的話,可參考以下指令:

    $ gcc -o point point.m main.m -lm -lobjc
    

    在這個指令中,-lobjc 參數是必要的,這樣 GCC 才會以 Objective-C 解析這些程式碼。

    如果讀者有閱讀過我們先前的文章,可發現我們這次沒有引入 GNUstep 相關的路徑。這是因為我們沒有繼承 NSObject,故不需要 GNUstep。

    如果想用 Clang 的話,指令基本上雷同:

    $ clang -o point point.m main.m -lm -lobjc
    

    雖然我們不用裝整個 GNUstep 物件庫,但我們仍然要裝 libobjc 函式庫,該函式庫包含了 Objecitve-C 物件的運行環境:

    $ sudo apt install libobjc-8-dev
    

    如果在非 Apple 平台上使用 Clang 編譯此範例程式,有可能會找不到 Objective-C 的運行環境。這時候要手動引入該標頭檔所在的位置:

    $ clang -o point point.m main.m -lm -lobjc -I/usr/lib/gcc/x86_64-linux-gnu/8/include
    

    結語

    在本文中,我們展示了一個不使用 Foundation 物件庫的 Objective-C 類別,當我們想用 Objective-C 的物件系統但不想相依於 Cocoa 或 GNUstep 時,就可以考慮用本文的方式來實作類別。

    由於本文的手法算是非主流,我們之後的範例不會刻意用這種方式來寫。本文就留給需要的讀者參考。

    【分享文章】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤網站】
    Facebook Facebook Twitter Plurk
    【支持本站】
    Buy me a coffeeBuy me a coffee