Menu
Coddy logo textTech

ポリモーフィックな反復処理

CoddyのCジャーニー「Object Oriented Programming」セクションの一部 — レッスン 42/61。

インターフェースを作成し、それらを異なる具体的な関数で実装する方法を学びました。本当のメリットは、異なるオブジェクトのコレクションを統一的に扱えるようになったときに得られます。具体的にどのような振る舞いが組み込まれているかに関わらず、それらを反復処理し、それぞれに対して同じ関数ポインタを呼び出すことができるようになるのです。

Action 構造体の配列があり、それぞれに独自の execute 関数が割り当てられていると想像してください:

typedef void (*ExecuteFunc)(void);

typedef struct {
    const char* name;
    ExecuteFunc execute;
} Action;

void jump(void) { printf("Jumping!\n"); }
void run(void)  { printf("Running!\n"); }
void rest(void) { printf("Resting...\n"); }

これらのアクションの配列を作成し、それぞれを異なる関数に関連付けてから、シンプルな for ループでそれらを反復処理することができます。

int main() {
    Action actions[3] = {
        { "Jump", jump },
        { "Run", run },
        { "Rest", rest }
    };
    
    for (int i = 0; i < 3; i++) {
        printf("%s: ", actions[i].name);
        actions[i].execute();
    }
    return 0;
}

ループは、各アクションが内部で何を行っているかを知ることも、気にすることもありません。単に各要素に対して execute() を呼び出すだけです。

これはポリモーフィズムの実際の動作です。同じコードが、まったく異なる振る舞いを持つオブジェクトを処理します。新しいアクションタイプを追加しても、反復処理のロジックを変更する必要はありません。配列に別の要素を追加するだけです。

challenge icon

チャレンジ

簡単

共通のインターフェースを共有しながら異なる振る舞いを実行するオブジェクトのコレクションを処理する、ポリモーフィックな反復処理を実演する Task 管理システムを構築しましょう。

コードを3つのファイルに分けて構成します:

  • task.h: ここでタスクのインターフェースを定義します。引数を取らず何も返さない TaskFunc という関数ポインタ型を作成します。次に、name (const char*) と TaskFunc 型の run 関数ポインタを含む Task 構造体を定義します。
  • task.c: 異なる種類の作業を表す3つの異なるタスク関数を実装します:
    • backup_taskBacking up data... と出力します
    • cleanup_taskCleaning up files... と出力します
    • report_taskGenerating report... と出力します
  • main.c: ここですべてをまとめます。それぞれが異なるタスク関数に紐付けられた Task 構造体の配列を作成します。次に、配列を反復処理し、各タスクをポリモーフィックに実行します。各タスクについて、その名前を出力した後に run 関数を呼び出します。

プログラムは単一の入力を受け取ります:実行するタスクの数 (1, 2, または 3) です。

次の順序で正確に3つのタスクを含む配列を作成してください:Backup という名前のバックアップタスク、Cleanup という名前のクリーンアップタスク、そして Report という名前のレポートタスクです。入力されたカウントに基づいて、配列の先頭からその数だけのタスクを反復処理します。

反復処理の各タスクについて、タスクの名前、続いてコロンとスペースを出力し、その後に run 関数を呼び出します。

入力が 2 の場合の出力例:

Backup: Backing up data...
Cleanup: Cleaning up files...

入力が 3 の場合の出力例:

Backup: Backing up data...
Cleanup: Cleaning up files...
Report: Generating report...

重要なポイントは、ループが各タスクが何をするかを知る必要がないということです。単に名前を出力し、各要素に対して run() を呼び出すだけです。同じ反復処理コードがすべてのタスクタイプを一律に処理します。ヘッダーファイルにはインクルードガードを使用することを忘れないでください。

チートシート

共通のインターフェースを共有するオブジェクトのコレクションを反復処理し、具体的な実装に関係なく各要素に対して同じ関数ポインタを呼び出すことができます。これがポリモーフィズム(多態性)であり、異なるオブジェクトを一様に扱うことです。

関数ポインタ型と、それを使用する構造体を定義します:

typedef void (*ExecuteFunc)(void);

typedef struct {
    const char* name;
    ExecuteFunc execute;
} Action;

シグネチャに一致する具体的な関数を作成します:

void jump(void) { printf("Jumping!\n"); }
void run(void)  { printf("Running!\n"); }
void rest(void) { printf("Resting...\n"); }

それぞれが異なる関数に関連付けられた構造体の配列を作成し、それらを反復処理します:

Action actions[3] = {
    { "Jump", jump },
    { "Run", run },
    { "Rest", rest }
};

for (int i = 0; i < 3; i++) {
    printf("%s: ", actions[i].name);
    actions[i].execute();
}

ループは各アクションが内部で何を行うかを知る必要はありません。単に各要素の関数ポインタを呼び出すだけです。新しい型を追加しても、反復処理のロジックを変更する必要はありません。

自分で試してみよう

#include <stdio.h>
#include "task.h"

// task.c で実装されているタスク関数を宣言します
void backup_task(void);
void cleanup_task(void);
void report_task(void);

int main() {
    int count;
    scanf("%d", &count);
    
    // TODO: 以下の順序で3つの Task 構造体の配列を作成します:
    // 1. backup_task を使用した、名前が "Backup" のバックアップタスク
    // 2. cleanup_task を使用した、名前が "Cleanup" のクリーンアップタスク
    // 3. report_task を使用した、名前が "Report" のレポートタスク
    
    // TODO: 配列から 'count' 個のタスクを反復処理します
    // 各タスクについて、"<name>: " を出力してから、その run 関数を呼び出します
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

Object Oriented Programmingのすべてのレッスン