由於 C 不直接支援多型,我們要用一些手法來模擬。在上一篇文章中,我們使用函式指標,在本文中,我們使用聯合 (union) 來模擬多型。
由於程式碼較長,我們將完整的程式碼放在這裡,有興趣的讀者可自行前往閱讀,本文僅節錄其中一部分。
首先來看如何使用具有多型特性的 Animal
類別:
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include "animal.h"
#include "dog.h"
int main(void)
{
// Create an array of quasi-polymorphic objects.
Animal *animals[] ={
animal_new(ANIMAL_TYPE_DUCK, "Michelle"),
animal_new(ANIMAL_TYPE_DOG, "Tommy"),
animal_new(ANIMAL_TYPE_TIGER, "Alice")
};
// Quasi-polymorphic calls.
for (size_t i = 0; i < 3; i++) {
printf("%s %s\n", animal_name(animals[i]), animal_speak(animals[i]));
}
// Extract Dog object from Animal object.
Dog *dog = (Dog *) animal_raw(animals[1]);
printf("Dog %s\n", dog_speak(dog));
// Quasi-polymorphically free memory.
for (size_t i = 0; i < 3; i++) {
animal_free(animals[i]);
}
return 0;
}
嚴格上來說,Animal
是單一型別,但內部具有多型的特性,我們於後文會展示其實作。我們刻意把 Dog
物件取出,只是用來展示 Animal
物件中藏著 Dog
物件。
接著,我們來看 Animal
類別的介面:
#ifndef ANIMAL_H
#define ANIMAL_H
typedef enum {
ANIMAL_TYPE_DUCK,
ANIMAL_TYPE_DOG,
ANIMAL_TYPE_TIGER
} Animal_t;
typedef struct animal Animal;
Animal * animal_new(Animal_t t, char *name);
char * animal_name(Animal *self);
char * animal_speak(Animal *self);
void * animal_raw(Animal *self);
void animal_free(void *self);
#endif // ANIMAL_H
單從介面來看,其實無法看出多型的部分。
但我們從 Animal
類別的宣告就可看出端倪:
struct animal {
Animal_t type;
union {
Dog *dog;
Duck *duck;
Tiger *tiger;
} _animal;
};
在 Animal
類別中,包著一個聯合,該聯合儲存 Dog *
、Duck *
、Tiger *
三者之一,並額外用 type
記錄目前實際的型別。從這個宣告就可以看出 Animal
類別的確有多型的精神在其中。
我們來看 Animal
類別的建構子:
Animal * animal_new(Animal_t t, char *name)
{
Animal *a = malloc(sizeof(Animal));
if (!a) {
perror("Unable to allocate animal a");
return a;
}
switch (t) {
case ANIMAL_TYPE_DOG:
a->type = ANIMAL_TYPE_DOG;
a->_animal.dog = dog_new(name);
if (!(a->_animal.dog)) {
perror("Unable to allocate dog");
goto ANIMAL_FREE;
}
break;
case ANIMAL_TYPE_DUCK:
a->type = ANIMAL_TYPE_DUCK;
a->_animal.duck = duck_new(name);
if (!(a->_animal.duck)) {
perror("Unable to allocate duck");
goto ANIMAL_FREE;
}
break;
case ANIMAL_TYPE_TIGER:
a->type = ANIMAL_TYPE_TIGER;
a->_animal.tiger = tiger_new(name);
if (!(a->_animal.tiger)) {
perror("Unable to allocate tiger");
goto ANIMAL_FREE;
}
break;
default:
assert("Invalid animal" && false);
}
return a;
ANIMAL_FREE:
free(a);
a = NULL;
return a;
}
其實這個建構子很像一個 Builder 類別,根據不同參數產生不同類別,只是我們將這個類別外部再用一個類別包起來。
我們來看其中一個公開方法:
char * animal_speak(Animal *self)
{
assert(self);
switch (self->type) {
case ANIMAL_TYPE_DOG:
return dog_speak(self->_animal.dog);
case ANIMAL_TYPE_DUCK:
return duck_speak(self->_animal.duck);
case ANIMAL_TYPE_TIGER:
return tiger_speak(self->_animal.tiger);
default:
assert("Invalid animal" && false);
}
}
Animal
類別本身不負責實際的行為,而由內部實際的類別決定其行為。最後的 default
敘述是一個防衛性措施,如果我們日後增加新的類別但卻忘了修改 switch
敘述的話,會引發錯誤。
最後來看 Animal
類別的解構子:
void animal_free(void *self)
{
if (!self) {
return;
}
switch (((Animal *) self)->type) {
case ANIMAL_TYPE_DOG:
dog_free(((Animal *) self)->_animal.dog);
break;
case ANIMAL_TYPE_DUCK:
duck_free(((Animal *) self)->_animal.duck);
break;
case ANIMAL_TYPE_TIGER:
tiger_free(((Animal *) self)->_animal.tiger);
break;
default:
assert("Invalid animal" && false);
}
free(self);
}
同樣也是要由內而外釋放記憶體。
由本文的實作,可知以下結果:
Animal
、Dog
、Duck
、Tiger
各自是可用的公開類別Animal
物件實際的行為由內部所有的物件來決定Dog
、Duck
、Tiger
各自是獨立的,三者間沒有子類型的關係Animal
是單一類別,但具有多型的特性
由本實作可看出,利用內嵌的聯合,的確可以創造有多型特性的物件和方法。
軟工的書會告訴我們,大量使用列舉搭配 switch
敘述是一種程式的壞味道 (bad smell),因為只要列舉的項目有所更動,程式設計者就要在許多地方修改 switch
敘述。其實本例也隱含一些些壞味道在裡面,只是由於程式碼短,故不明顯;至於要不要使用這樣的特性,就請讀者自行衡量。