【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:多态 (Polymorphism),使用联合 (Union)

【赞助商连结】

    由于 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 叙述。其实本例也隐含一些些坏味道在里面,只是由于程序代码短,故不明显;至于要不要使用这样的特性,就请读者自行衡量。

    【赞助商连结】