[C 語言] 程式設計教學:多型 (Polymorphism),使用聯合 (Union)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
【贊助商連結】

    由於 C 不直接支援多型,我們要用一些手法來模擬。在上一篇文章中,我們使用函式指標,在本文中,我們使用聯合 (union) 來模擬多型。

    注意:本文的 C 程式碼是合法的,但也算是一種反模式 (anti-pattern),請各位讀者小心服用。

    由於程式碼較長,我們將完整的程式碼放在這裡,有興趣的讀者可自行前往閱讀,本文僅節錄其中一部分。

    首先來看如何使用具有多型特性的 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, "Michael"),
            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 敘述。其實本例也隱含一些些壞味道在裡面,只是由於程式碼短,故不明顯;至於要不要使用這樣的特性,就請讀者自行衡量。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【支持站長】
    Buy me a coffeeBuy me a coffee
    【贊助商連結】
    【分類瀏覽】