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

PUBLISHED ON OCT 11, 2018 — PROGRAMMING
FacebookTwitter 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 *p = person_new("Michael", 37);
        IPerson *ipp = person_to_iperson(p);
    
        Employee *ee = employee_new("Tommy", 28, "Google", 1000);
        IPerson *ipee = employee_to_iperson(ee);
    
        IPerson *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_free(ipp);
        iperson_free(ipee);
    
        person_free(p);
        employee_free(ee);
    
        return EXIT_SUCCESS;
    }
    

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

    // 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 類別,這個類別是 PersonEmployee 共通的介面:

    #ifndef IPERSON_H
    #define IPERSON_H
    
    typedef struct iperson {
        char* (*name) (void *self);
        void (*set_name) (void *self, char *name);
        unsigned int (*age) (void *self);
        void (*set_age) (void *self, unsigned int age);
    } IPerson;
    
    void iperson_free(void *self);
    
    #endif  // IPERSON_H
    

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

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

    #ifndef PERSON_H
    #define PERSON_H
    
    #include "iperson.h"
    
    typedef struct person Person;
    
    Person* person_new(char *name, unsigned int age);
    char* person_name(Person *self);
    void person_set_name(Person *self, char *name);
    unsigned int person_age(Person *self);
    void person_set_age(Person *self, unsigned int age);
    void person_free(void *self);
    IPerson* person_to_iperson(Person* self);
    
    #endif  // PERSON_H
    

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

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

    IPerson* person_to_iperson(Person* self)
    {
        IPerson* ip = malloc(sizeof(IPerson));
    
        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 *) self);
    }
    
    static void _set_name(void *self, char *name)
    {
        person_set_name((Person *) self, name);
    }
    
    static unsigned int _age(void *self)
    {
        return person_age((Person *) self);
    }
    
    static void _set_age(void *self, unsigned int age)
    {
        person_set_age((Person *) self, age);
    }
    

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

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

    #ifndef EMPLOYEE_H
    #define EMPLOYEE_H
    
    #include "iperson.h"
    
    typedef struct employee Employee;
    
    Employee* employee_new(
        char *name, unsigned int age, char *company, double salary);
    char* employee_name(Employee *self);
    void employee_set_name(Employee *self, char *name);
    unsigned int employee_age(Employee *self);
    void employee_set_age(Employee *self, unsigned int age);
    char* employee_company(Employee *self);
    void employee_set_company(Employee *self, char *company);
    double employee_salary(Employee *self);
    void employee_set_salary(Employee *self, double salary);
    void employee_free(void *self);
    IPerson* employee_to_iperson(Employee *self);
    
    #endif // EMPLOYEE_H
    

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

    我們節錄 Employee 類別中和 IPerson 類別相關的部分:

    IPerson* employee_to_iperson(Employee *self)
    {
        IPerson *ip = malloc(sizeof(IPerson));
    
        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 *) self);
    }
    
    static void _set_name(void *self, char *name)
    {
        employee_set_name((Employee *) self, name);
    }
    
    static unsigned int _age(void *self)
    {
        return employee_age((Employee *) self);
    }
    
    static void _set_age(void *self, unsigned int age)
    {
        employee_set_age((Employee *) self, age);
    }
    

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

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

    • PersonEmployee 都是可用的公開類別
    • Employee 內部會呼叫 Person
    • PersonEmployee 沒有子類別的關係
    • IPersonPersonEmployee 共用的介面

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