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

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

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。