說明
在物件導向程式中,透過繼承可重用程式碼,有繼承關係的類別的資料型態可以相容。在 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
初始化時會用到寬和高,所以在建構式中引入這兩個參數。
除了滿足 perimeter
和 area
兩個來自 Shape
的公開訊息外,Rectangle
額外實作了 width
和 height
兩個特有的公開訊息。
以下是 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
如同 Rectangle
,Circle
(圓形) 繼承了 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
的公開訊息 perimeter
和 area
外,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
這裡沒用到什麼複雜的演算法,讀者應可自行閱讀。
使用 Rectangle
和 Circle
的外部程式
最後,我們寫一個外部程式來使用 Rectangle
和 Circle
物件:
/* 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
而不是用 Rectangle
或 Circle
。因為後兩者是前者的衍生類別,可共享基礎類別的資料型態。
評語
在這個例子中,Shape
並不提供實作,只是規範了抽象訊息。實際上 Rectangle
和 Circle
得到是 Shape
繼承自 NSObject
的基礎類別特性。這時候 Shape
的目的是提供共通的資料型態。
由於 Objective-C 採用單一繼承,只為了共通的資料型態就占掉基礎類別其實有點浪費。雖然在語法特性上可以用,實務上並不建議這麼做。比較好的方式是用 protocal
做為類別的公開約定。我們會在下一篇文章介紹 protocal
的使用方式。