Menu
Coddy logo textTech

Полиморфная итерация

Часть раздела Object Oriented Programming путешествия по C на Coddy — урок 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, которая демонстрирует полиморфную итерацию — обработку коллекции объектов, имеющих общий интерфейс, но выполняющих разные действия.

Вы организуете свой код в трех файлах:

  • task.h: Определите здесь интерфейс задачи. Создайте тип указателя на функцию с именем TaskFunc, который не принимает параметров и ничего не возвращает. Затем определите структуру Task, содержащую name (типа const char*) и указатель на функцию run типа TaskFunc.
  • task.c: Реализуйте три различные функции задач, которые представляют разные виды работ:
    • backup_task — выводит Backing up data...
    • cleanup_task — выводит Cleaning up files...
    • report_task — выводит Generating report...
  • main.c: Соберите все вместе здесь. Создайте массив структур Task, каждая из которых связана с соответствующей функцией задачи. Затем выполните итерацию по массиву и выполните каждую задачу полиморфно — для каждой задачи выведите ее имя, а затем вызовите ее функцию run.

Ваша программа получит на вход одно значение: количество задач для запуска (1, 2 или 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() для каждого элемента. Один и тот же код итерации единообразно обрабатывает все типы задач. Не забудьте использовать include guards в заголовочном файле.

Шпаргалка

Вы можете перебирать коллекции объектов, имеющих общий интерфейс, вызывая один и тот же указатель на функцию для каждого элемента, независимо от его конкретной реализации. Это и есть полиморфизм — единообразная работа с различными объектами.

Определите тип указателя на функцию и структуру, которая его использует:

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", использующая backup_task
    // 2. Задача очистки с именем "Cleanup", использующая cleanup_task
    // 3. Report task named "Report" using report_task
    
    // TODO: Переберите 'count' задач из массива
    // Для каждой задачи выведите: "<name>: ", затем вызовите её функцию run
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Object Oriented Programming