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

PUBLISHED ON OCT 11, 2018 — PROGRAMMING

在物件導向設計中,多型 (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 語言的限制,子類型是無法取得的特性,但我們藉由一些額外的樣板程式碼,達到多型的特性。

comments powered by Disqus