位元詩人 [Objective-C] 程式設計教學:繼承類別的方式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

說明

在物件導向程式中,透過繼承可重用程式碼,有繼承關係的類別的資料型態可以相容。在 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 的使用方式。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。