繼承類別的方式

    說明

    在物件導向程式中,透過繼承可重用程式碼,有繼承關係的類別的資料型態可以相容。在 Objective-C 中,有兩種繼承類別的方式:

    • 將目標類別設為基礎類別 (base class)
    • 用 category 擴展特定類別

    我們一開始學寫 Objective-C 的類別時,就用到第一種方式了。因為每個 Objective-C 類別至少會繼承 NSObject 類別,以取得類別的基本特性。只有在少數情形下,才會有完全不繼承任何類別的 Objective-C 類別。

    Objective-C 是建置在 C 之上的物件系統,而且是 C 的嚴格超集合。在不破壞 C 的核心特性的前提下,NSObject 這種基礎物件本應是程式語言的基本特性,在 Objective-C 只能用物件庫的形式外加在程式中。

    本文會以範例展示第一種方式。至於第二種方式則留在多型的章節來說明。

    建立基礎類別 Shape

    Shape (形狀) 是代表幾何圖形的基礎類別。其公開界面如下:

    /* shape.h */
    #pragma once
    
    #import <Foundation/Foundation.h>
    
    
    @interface Shape : NSObject
    -(double) perimeter;
    -(double) area;
    @end
    

    本例的 Shape 的公開訊息只有 perimeter (周長) 和 area (面積),因為每個幾何圖形都會有這些性質。至於常見的寬 (width) 和高 (height) 並不是 Shape 的公開訊息,因為有些幾何圖形沒有寬和高。

    如同其他的 Objective-C 類別,Shape 也繼承了 NSObject,以取得類別的基本特性。

    以下是 Shape 的內部實作:

    /* shape.m */
    #import <Foundation/Foundation.h>
    #import "shape.h"
    
    
    @implementation Shape
    -(double) perimeter
    {
        /* Simulate an abstract message. */
        [self doesNotRecognizeSelector: \
            @selector(perimeter)];
    
        /* Trick for compiler warning. */
        return 0.0;
    }
    
    -(double) area
    {
        /* Simulate an abstract message. */
        [self doesNotRecognizeSelector: \
            @selector(area)];
    
        /* Trick for compiler warning. */
        return 0.0;
    }
    @end
    

    本範例的 Shape 定位為抽象類別 (abstract class),所以所有的訊息都是抽象訊息。由於 Objective-C 不支援抽象訊息,替代的實作方式是在訊息的實作埋例外事件。呼叫到該訊息時就觸發該例外事件,直接終止掉程式。

    建立例外事件的方式是使用 doesNotRecognizeSelector:,該訊息會觸發 NSInvalidArgumentException,以強制中止程式。

    doesNotRecognizeSelector: 訊息中把 selector (訊息的資料型態) 當資料在操作的手法,算是元程式設計 (metaprogramming) 的範圍之一。這種程式設計範式對初學者來說比較抽象一些。

    繼承 Shape 的衍生類別 Rectangle

    Rectangle (長方形) 繼承了 Shape,也就間接繼承了 NSObject。所以,Rectange 也可以使用 NSObject 所提供的類別基本特性。

    Rectangle 的公開界面如下:

    /* rectangle.h */
    #pragma once
    
    #import <Foundation/Foundation.h>
    #import "shape.h"
    
    
    @interface Rectangle : Shape {
        double width;
        double height;
    }
    
    -(Rectangle *) initWithWidth: (double) w andHeight: (double) h;
    -(double) width;
    -(double) height;
    -(double) perimeter;
    -(double) area;
    @end
    

    Rectangle 初始化時會用到寬和高,所以在建構式中引入這兩個參數。

    除了滿足 perimeterarea 兩個來自 Shape 的公開訊息外,Rectangle 額外實作了 widthheight 兩個特有的公開訊息。

    以下是 Rectangle 的內部實作:

    /* rectangle.m */
    #import <Foundation/Foundation.h>
    #import "rectangle.h"
    
    
    @implementation Rectangle
    -(Rectangle *) initWithWidth: (double) w andHeight: (double) h
    {
        NSAssert(w > 0.0, @"The width of a rectangle should be larger than zero");
        NSAssert(h > 0.0, @"The height of a rectangle should be larger than zero");
    
        if (!self)
            return self;
    
        self = [super init];
    
        width = w;
        height = h;
    
        return self;
    }
    
    -(double) width
    {
        return width;
    }
    
    -(double) height
    {
        return height;
    }
    
    -(double) perimeter
    {
        return (width + height) * 2;
    }
    
    -(double) area
    {
        return width * height;
    }
    @end
    

    實作 Rectangle 時會用到基礎幾何的知識。實作上都很簡單,讀者可自行閱讀。

    繼承 Shape 的衍生類別 Circle

    如同 RectangleCircle (圓形) 繼承了 Shape,因而間接繼承了 NSObject

    以下是 Circle 的公開界面:

    /* circle.h */
    #pragma once
    
    #import <Foundation/Foundation.h>
    #import "shape.h"
    
    
    @interface Circle : Shape {
        double radius;
    }
    
    -(Circle *) initWithRadius: (double) r;
    -(double) radius;
    -(double) perimeter;
    -(double) area;
    @end
    

    Circle 建立時需要半徑 (radius),故將半徑做為建構式的參數傳入。

    除了滿足來自 Shape 的公開訊息 perimeterarea 外,Circle 另外宣告了特有的公開訊息 radius (半徑)。

    以下是 Circle 的內部實作:

    /* circle.m */
    #import <Foundation/Foundation.h>
    #import "circle.h"
    
    
    @implementation Circle
    -(Circle *) initWithRadius: (double)r
    {
        NSAssert(r > 0.0, @"The radius of a circle should be larger than zero");
    
        if (!self)
            return self;
    
        self = [super init];
    
        radius = r;
    
        return self;
    }
    
    -(double) radius
    {
        return radius;
    }
    
    -(double) perimeter
    {
        return 2 * radius * M_PI;
    }
    
    -(double) area
    {
        return radius * radius * M_PI;
    }
    @end
    

    這裡沒用到什麼複雜的演算法,讀者應可自行閱讀。

    使用 RectangleCircle 的外部程式

    最後,我們寫一個外部程式來使用 RectangleCircle 物件:

    /* main.m */
    #import <Foundation/Foundation.h>
    #import "shape.h"
    #import "rectangle.h"
    #import "circle.h"
    
    
    int main(void)
    {
        NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
        if (!pool)
            return 1;
    
        Shape *rec = [[[Rectangle alloc]
            initWithWidth: 3.0 andHeight: 4.0]
                autorelease];
        if (!rec)
            goto ERROR_MAIN;
    
        if (!([rec perimeter] - 14.0 < 0.00001)) {
            fprintf(stderr, "Wrong rectangle perimeter\n");
            goto ERROR_MAIN;
        }
    
        if (!([rec area] - 12.0 < 0.00001)) {
            fprintf(stderr, "Wrong rectangle area\n");
            goto ERROR_MAIN;
        }
    
        Shape *c = \
            [[[Circle alloc] initWithRadius: 10.0]
                autorelease];
        if (!c)
            goto ERROR_MAIN;
    
        if (!([c perimeter] - 2 * 10.0 * M_PI < 0.00001)) {
            fprintf(stderr, "Wrong circle perimeter\n");
            goto ERROR_MAIN;
        }
    
        if (!([c area] - 10.0 * 10.0 * M_PI < 0.00001)) {
            fprintf(stderr, "Wrong circle area\n");
            goto ERROR_MAIN;
        }
    
        [pool drain];
    
        return 0;
    
    ERROR_MAIN:
        [pool drain];
    
        return 1;
    }
    

    注意我們的資料型態是用 Shape 而不是用 RectangleCircle。因為後兩者是前者的衍生類別,可共享基礎類別的資料型態。

    評語

    在這個例子中,Shape 並不提供實作,只是規範了抽象訊息。實際上 RectangleCircle 得到是 Shape 繼承自 NSObject 的基礎類別特性。這時候 Shape 的目的是提供共通的資料型態。

    由於 Objective-C 採用單一繼承,只為了共通的資料型態就占掉基礎類別其實有點浪費。雖然在語法特性上可以用,實務上並不建議這麼做。比較好的方式是用 protocal 做為類別的公開約定。我們會在下一篇文章介紹 protocal 的使用方式。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter