開源技術教學文件網 多型 (Polymorphism),使用聯合 (Union)

最後修改日期為 MAY 2, 2021

由於 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);
}

同樣也是要由內而外釋放記憶體。

由本文的實作,可知以下結果:

  • AnimalDogDuckTiger 各自是可用的公開類別
  • Animal 物件實際的行為由內部所有的物件來決定
  • DogDuckTiger 各自是獨立的,三者間沒有子類型的關係
  • Animal 是單一類別,但具有多型的特性

由本實作可看出,利用內嵌的聯合,的確可以創造有多型特性的物件和方法。

軟工的書會告訴我們,大量使用列舉搭配 switch 敘述是一種程式的壞味道 (bad smell),因為只要列舉的項目有所更動,程式設計者就要在許多地方修改 switch 敘述。其實本例也隱含一些些壞味道在裡面,只是由於程式碼短,故不明顯;至於要不要使用這樣的特性,就請讀者自行衡量。

電子書籍

如果你覺得這篇 C 語言的技術文章對你有幫助,可以看看這本 C 語言應用程式設計電子書:

C 語言應用程式設計

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