[C 語言] 程式設計教學:多型 (Polymorphism),使用函式指標

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    在物件導向設計中,多型 (polymorphism) 是將同一個界面套用在不用的類別上。有以下數種實踐方式:

    • Ad hoc polymorphism:在許多程式中使用函式重載 (function overloading) 來實踐
    • Parametric polymorphism:在程式設計中用泛型 (generics) 來實踐
    • Subtyping:使用繼承來實踐

    多型的公開界面成為公開的約定 (contract),在設計模式中就有許多使用多型的例子。

    基本上,C 也缺乏對多型的直接支援,要用一些方法去模擬。在本文中,我們使用函式指標的方式去模擬多型;由於完整的程式碼較長,請讀者到這裡觀看,我們僅節錄相關的部分。

    先看多型的使用方式:

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include "person.h"
    #include "employee.h"
    #include "iperson.h"
    
    int main()
    {
        person_t *p = person_new("Michael", 37);
        if (!p) {
            perror("Failed to allocate p\n");
            goto ERROR;
        }
    
        iperson_t *ipp = person_to_iperson();
        if (!ipp) {
            perror("Failed to allocate ipp\n");
            goto ERROR;
        }
    
        employee_t *ee = employee_new("Tommy", 28, "Google", 1000);
        if (!ee) {
            perror("Failed to allocate ee\n");
            goto ERROR;
        }
    
        iperson_t *ipee = employee_to_iperson();
        if (!ipee) {
            perror("Failed to allocate ipee\n");
            goto ERROR;
        }
    
        iperson_t *ips[] = {ipp, ipee};
        void *objs[] = {(void *) p, (void *) ee};
    
        // Polymorphic calls.
        for (int i = 0; i < 2; i++) {
            printf("Name: %s\n", ips[i]->name(objs[i]));
            printf("Age: %d\n", ips[i]->age(objs[i]));
        }
    
        // Mutate p.
        ipp->set_name(p, "Mike");
        ipp->set_age(p, 39);
    
        // Mutate ee.
        ipee->set_name(ee, "Tom");
        ipee->set_age(ee, 30);
    
        // Mutate ee with non-polymorphic call here.
        employee_set_company(ee, "Microsoft");
        employee_set_salary(ee, 1200);
    
        printf("\n"); // Separator.
    
        // Polymorphic calls again.
        for (int i = 0; i < 2; i++) {
            printf("Name: %s\n", ips[i]->name(objs[i]));
            printf("Age: %d\n", ips[i]->age(objs[i]));
        }
    
        iperson_delete(ipp);
        iperson_delete(ipee);
    
        person_delete(p);
        employee_delete(ee);
    
        return 0;
    
    ERROR:
        if (ipee)
            iperson_delete(ipee);
    
        if (ee)
            employee_delete(ee);
    
        if (ipp)
            iperson_delete(ipp);
    
        if (p)
            person_delete(p);
        
        return 1;
    }
    

    在本例中,以下的部分有用到多型的概念:

    // Polymorphic calls.
    for (int i = 0; i < 2; i++) {
        printf("Name: %s\n", ips[i]->name(objs[i]));
        printf("Age: %d\n", ips[i]->age(objs[i]));
    }
    

    在陣列中,objs 是不同的型別,但可用相同的界面來呼叫,精神上也是一種多型。由於 C 語言沒有直接支援多型的語法,無法像 Java 般直接套個介面 (interface) 就有多型了,而要多寫一些樣板 (boilerplate) 程式碼。

    本實作的關鍵在於我們額外建立一個 iperson_t 類別,這個類別是 person_temployee_t 共通的介面:

    #pragma once
    
    typedef struct iperson_t iperson_t;
    
    struct iperson_t {
        char* (*name) (void *self);
        void (*set_name) (void *self, char *name);
        unsigned int (*age) (void *self);
        void (*set_age) (void *self, unsigned int age);
    };
    
    void iperson_delete(void *self);
    

    在這個介面中,我們宣告了 4 個方法,這個方法是 person_temployee_t 共有的部分。

    接著我們來看 person_t 類別的介面:

    #pragma once
    
    #include "iperson.h"
    
    typedef struct person_t person_t;
    
    person_t* person_new(char *name, unsigned int age);
    char* person_name(person_t *self);
    void person_set_name(person_t *self, char *name);
    unsigned int person_age(person_t *self);
    void person_set_age(person_t *self, unsigned int age);
    void person_delete(void *self);
    
    iperson_t* person_to_iperson();
    

    在這個版本的 person_t 介面中,我們額外加入一個 person_to_iperson 的方法,進行型別轉換。

    我們把 person_t 類別中關鍵的部分節錄出來:

    iperson_t* person_to_iperson()
    {
        iperson_t* ip = malloc(sizeof(iperson_t));
    
        ip->name = _name;
        ip->set_name = _set_name;
        ip->age = _age;
        ip->set_age = _set_age;
    
        return ip;
    }
    
    static char* _name(void *self)
    {
        return person_name((person_t *) self);
    }
    
    static void _set_name(void *self, char *name)
    {
        person_set_name((person_t *) self, name);
    }
    
    static unsigned int _age(void *self)
    {
        return person_age((person_t *) self);
    }
    
    static void _set_age(void *self, unsigned int age)
    {
        person_set_age((person_t *) self, age);
    }
    

    在這個版本的 person_t 類別中,除了實作 person_t 原先的方法,我們還實作了將 person_tiperson_t 的方法,並將 iperson_t 中相關的公開方法指向 person_t 內部特定的實作。

    接著,我們來看 employee_t 的介面:

    #pragma once
    
    #include "iperson.h"
    
    typedef struct employee_t employee_t;
    
    employee_t* employee_new(
        char *name, unsigned int age, char *company, double salary);
    char* employee_name(employee_t *self);
    void employee_set_name(employee_t *self, char *name);
    unsigned int employee_age(employee_t *self);
    void employee_set_age(employee_t *self, unsigned int age);
    char* employee_company(employee_t *self);
    void employee_set_company(employee_t *self, char *company);
    double employee_salary(employee_t *self);
    void employee_set_salary(employee_t *self, double salary);
    void employee_delete(void *self);
    
    iperson_t* employee_to_iperson();
    

    同樣地,在這個版本的 employee_t 中,也多出一個 employee_to_iperson 的方法。

    我們節錄 employee_t 類別中和 iperson_t 類別相關的部分:

    iperson_t* employee_to_iperson()
    {
        iperson_t *ip = malloc(sizeof(iperson_t));
    
        ip->name = _name;
        ip->set_name = _set_name;
        ip->age = _age;
        ip->set_age = _set_age;
    
        return ip;
    }
    
    static char* _name(void *self)
    {
        return employee_name((employee_t *) self);
    }
    
    static void _set_name(void *self, char *name)
    {
        employee_set_name((employee_t *) self, name);
    }
    
    static unsigned int _age(void *self)
    {
        return employee_age((employee_t *) self);
    }
    
    static void _set_age(void *self, unsigned int age)
    {
        employee_set_age((employee_t *) self, age);
    }
    

    同樣地,iperson_t 類別本身沒有實作,而由 employee_t 類別負責實際的實作。

    根據我們的實作,有以下的結果:

    • person_temployee_t 都是可用的公開類別
    • employee_t 內部會呼叫 person_t
    • person_temployee_t 沒有子類別的關係
    • iperson_tperson_temployee_t 共用的介面

    由於 C 語言的限制,子類型是無法取得的特性,但我們藉由一些額外的樣板程式碼,達到多型的特性。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk