在物件導向設計中,多型 (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_t
和 employee_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_t
和 employee_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_t
轉 iperson_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_t
和employee_t
都是可用的公開類別employee_t
內部會呼叫person_t
person_t
和employee_t
沒有子類別的關係iperson_t
是person_t
和employee_t
共用的介面
由於 C 語言的限制,子類型是無法取得的特性,但我們藉由一些額外的樣板程式碼,達到多型的特性。