Возвращение статусного кода ошибки

Последнее обновление: 08.09.2023

Иногда необходимо, чтобы функция уведомляла внешний контекст о результатах своей работы на различных ее этапах. В этом случае распространенным решением является возврат из функции статусных кодов. Получив из функции статусный код, вызывающий код может его обработать по своему усмотрению. В качестве статусного кода результата часто используются числовые значения. Например, в Windows Microsoft использует тип HRESULT, который фактически представляет 64-разрядное число и применяется для возвращения статусного кода результата.

Так, возьмем следующую программу, которая ищет в файле определенное слово:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
void find(char*, char*, size_t);
void find_word(char*, char*, size_t, FILE*);
 
int main(void){
    char* filename ="test.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    find(filename, word, size);
}
 
void find_word(char* buffer, char* word, size_t word_size, FILE* file_pointer){
    int found = 0; // индикатор, что слово найдено
    while(fgets(buffer, word_size, file_pointer))
    {
        if(strcmp(word, buffer)==0)
        {
            found = 1;
            break;
        }
    }
    if(found) 
    {
        printf("Success! Word found!\n");
    }
    else
    {
        printf("Word not found...\n");
    }
}
 
void find(char* file_name, char* word, size_t word_size)
{
    FILE* file_pointer = 0;
    char* buffer = 0;
 
    if(!file_name) // если не указано имя файла
    {
        printf("Invalid filename\n");
        return;
    }
    if(!word) // если не указано слово для поиска
    {
        printf("Invalid word for search\n");
        return;
    }
     if(!(file_pointer=fopen(file_name, "r")))  // 
    {
        printf("Unable to open the file\n");
        return;
    }
    if((buffer=malloc(word_size)))      // если память выделена
    {
        find_word(buffer, word, word_size, file_pointer);
        free(buffer);
    }
    else     // если не удалось выделить память
    {
        printf("Error! Unable to locate memory!\n");
    }
    fclose(file_pointer);
}

Здесь функция find() получает извне имя файла для поиска, слово для поиска и размер слова. Чтобы найти слово, открываем файл и затем выделяем буфер в памяти для считывания данных. Затем вызывается функция find_word(), которая считывает символы из файла в буфер и проверяет на их соответствие искомому слову. Здесь есть несколько узловых точек, каждая из которых обрабатывает определенное состояние. Например, если переданное в функцию имя файла не указано, то выводим сообщение об ошибке и выходим из функции:

if(!file_name) // если не указано имя файла
{
    printf("Invalid filename\n");
    return;
}

Похожим способом обрабатываются другие ситуации - не указано слово для поиска, не удалось открыть файл, не удалось выделить память для считывания слова. Отдельно стоит сказать про функцию find_word(), где для хранения результата пришлось создавать отдельную переменную и в зависимости от ее значения выводим одно или другое сообщение (то есть еще два состояния):

int found = 0; // индикатор, что слово найдено
while(fgets(buffer, word_size, file_pointer))
{
    if(strcmp(word, buffer)==0)
    {
        found = 1;
        break;
    }
}
if(found) 
{
    printf("Success! Word found!\n");
}
else
{
    printf("Word not found...\n");
}

Теперь определим статусные коды для каждого состояния и вместо обработки на месте, возвратим статусный код и предоставим обработку вызывающему коду:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef enum{
    FILENAME_INVALID,
    WORD_INVALID,
    FILEOPEN_ERROR,
    ALLOCATION_ERROR,
    WORD_FOUND,
    WORD_NOTFOUND
} status;

status find(char*, char*, size_t);
status find_word(char*, char*, size_t, FILE*);
 
int main(void){
    char* filename ="test.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    status result = find(filename, word, size);
    switch (result)
    {
        case FILENAME_INVALID:
            printf("Invalid filename\n");
            break;
        case WORD_INVALID:
            printf("Invalid word\n");
            break;
        case FILEOPEN_ERROR:
            printf("Unable to open the file\n");
            break;
        case ALLOCATION_ERROR:
            printf("Unable to allocate memory\n");
            break;
        case WORD_FOUND:
            printf("Success! Word found!\n");
            break;
        case WORD_NOTFOUND:
            printf("Word not found...\n");
            break;
    }
}      
 
status find_word(char* buffer, char* word, size_t word_size, FILE* file_pointer){
    while(fgets(buffer, word_size, file_pointer))
    {
        if(strcmp(word, buffer)==0)
        {
            return WORD_FOUND;
        }
    }
    return WORD_NOTFOUND;
}
 
status find(char* file_name, char* word, size_t word_size)
{
    FILE* file_pointer = 0;
    char* buffer = 0;
    status result;
 
    if(!file_name) // если не указано имя файла
    {
        return FILENAME_INVALID;
    }
    if(!word) // если не указано слово для поиска
    {
        return WORD_INVALID;
    }
    if(!(file_pointer=fopen(file_name, "r")))
    {
        return FILEOPEN_ERROR;
    }
    if((buffer=malloc(word_size)))      // если память выделена
    {
        result = find_word(buffer, word, word_size, file_pointer);
        free(buffer);
    }
    else     // если не удалось выделить память
    {
        result = ALLOCATION_ERROR;
    }
    fclose(file_pointer);
    return result;
}

Теперь для хранения результата определено перечисление status с 6-ю возможными состояниями. В функции find и find_word в зависимости от условий устанавливаем и возвращаем результат. В функции main, получив статусный код, обрабатываем его в конструкции switch - выводим то или иное сообщение.

Таким образом, мы можем отделить логику обработки результата от основного действия функций find/find_word. И в то же время вызывающий код с помощью результата получит статус и должным образом обработает его.

Вместо конструкции switch для обработки статуса можно использовать таблицы, например, изменим код функции main следующим образом:

int main(void){
    char* filename ="test.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    status result = find(filename, word, size);
    char* status_messages[6] = 
    {
        "Invalid filename\n",
        "Invalid word\n",
        "Unable to open the file\n",
        "Unable to allocate memory\n",
        "Success! Word found!\n",
        "Word not found...\n"
    };
    printf(status_messages[result]);
}     

Здесь все выводимые сообщения определены в массиве строк status_messages. Поскольку значение перечисления по сути представляет число, причем по умолчанию первая константа в перечислении равна 0, а остальные увеличиваются на единицу, то мы можем использовать статусный код в качестве индекса в массиве для получения нужного сообщения для ее последующего вывода на консоль.

В то же время у возвращения статусных кодов есть и недостатки. Прежде всего функция и вызывающий ее контекст (в примере выше функция main) должны следовать единому API статусных кодов. Кроме того, вызывающий код должен быть осведомлен о смысле и семантике тех или иных кодов, в случае если вызываемую функцию создает другой программист.

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

Релевантность статусных кодов

Еще одна проблема, связанная с возвращением информации об ошибки, связана с релевантностью информации. Например, в примере выше, если не удалось выделить память для буфера, то функция find возвращает значение ALLOCATION_ERROR. Имеет ли значение для функции main, что ошибка произошла именно в результате того, что не удалось выделить память? То есть насколько релевантно этот тип статусных кодом для вызывающего кода - функции main? В каждой конкретной ситуации понятие релевантности может различаться. Возможно, для создателя функции main все ошибки будут аналогичны, и он может даже не обрабатывать все отдельные типы статусных кодов:

int main(void){
    char* filename ="test21.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    status result = find(filename, word, size);

    switch (result)
    {
        case WORD_FOUND:
            printf("Success! Word found!\n");
            break;
        case WORD_NOTFOUND:
            printf("Word not found...\n");
            break;
        default: 
            printf("Error occured...\n");
            break;
    }
}    

В данном случае для всех статусных кодов, отличных от WORD_FOUND и WORD_NOTFOUND выводится одно сообщение об ошибке.

С точки зрения проектировщика функции find мы можем возвращать широкую палитру статусных кодов. Однако если эти статусные коды в принципе не будут использоваться, то нет смысла перегружать функцию возвращением всех этих кодов, а возвращать только такую информацию, которая релевантна для потребителя функции find. Например:

#include 
#include 
#include 
 
typedef enum{
    ERROR,
    WORD_FOUND,
    WORD_NOTFOUND
} status;

status find(char*, char*, size_t);
status find_word(FILE*, char*, size_t, char*);
 
int main(void){
    char* filename ="test.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    status result = find(filename, word, size);

    switch (result)
    {
        case WORD_FOUND:
            printf("Success! Word found!\n");
            break;
        case WORD_NOTFOUND:
            printf("Word not found...\n");
            break;
        default: 
            printf("Error occured...\n");
            break;
    }
}      

// поиск слова
status find_word(FILE* file_pointer, char* word, size_t word_size, char* buffer){

    while(fgets(buffer, word_size, file_pointer)){
        
        if(strcmp(word, buffer)==0){ 

            return WORD_FOUND;   // если нашли слово
        }
    }
    return WORD_NOTFOUND;        // если не нашли слово
}
 
status find(char* file_name, char* word, size_t word_size)
{
    FILE* file_pointer = 0;
    char* buffer = 0;
    status result = ERROR;
 
    // если file_name != NULL и word != NULL и файл октрыт, и память выделена
    if(file_name && word && (file_pointer=fopen(file_name, "r")) && (buffer=malloc(word_size))) 
    {
        result = find_word(file_pointer, word, word_size, buffer); // собственно поиск слова
    }
        
    // секция освобождения ресурсов
    if(buffer) free(buffer);
    if(file_pointer) fclose(file_pointer);

    return result;
}

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

В случае с проверкой параметров на null в принципе можно было бы использовать assert:

// если не соблюдаются предусловия, завершаем функцию
assert(file_name!=NULL && "File name is invalid!");
assert(word!=NULL && "Word is invalid!");

// если удалось открыть файл и выделить память
if((file_pointer=fopen(file_name, "r")) && (buffer=malloc(word_size))) 
{
    result = find_word(file_pointer, word, word_size, buffer);
}

Однако иногда вообще не требуется какой-то сложной системы статусных кодов и бывает достаточно двух состояний - успех или ошибка. И нередко можно встретить функции, которые возвращают либо true или false, 1 или 0 (-1), вообщем одно из двух значений. Так, в примере выше может быть достаточно, найдено слово или нет. А была выделена память или нет, был ли открыт файл или нет, корректные ли параметры, все это может не играть роли, например:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int find(char*, char*, size_t);
int find_word(FILE*, char*, size_t, char*);
 
int main(void){
    char* filename ="test32.txt";
    char word[] = "hello";
    size_t size = sizeof(word);
 
    if(find(filename, word, size))
    {
        printf("Success! Word found!\n");
    }
    else
    {
        printf("Word not found...\n");
    }
}      

// поиск слова
int find_word(FILE* file_pointer, char* word, size_t word_size, char* buffer)
{
    while(fgets(buffer, word_size, file_pointer))
    {    
        if(strcmp(word, buffer)==0)
        {
            return 1;   // если нашли слово
        }
    }
    return 0;        // если не нашли слово
}
 
int find(char* file_name, char* word, size_t word_size)
{
    FILE* file_pointer = 0;
    char* buffer = 0;
    int result = 0; // возвращаемый результат
 
    // если не соблюдаются предусловия, завершаем функцию
    assert(file_name!=NULL && "File name is invalid!");
    assert(word!=NULL && "Word is invalid!");

   // если удалось открыть файл и выделить память
    if((file_pointer=fopen(file_name, "r")) && (buffer=malloc(word_size))) 
    {
        result = find_word(file_pointer, word, word_size, buffer); // собственно поиск слова
    }
        
    // секция освобождения ресурсов
    if(buffer) free(buffer);
    if(file_pointer) fclose(file_pointer);

    return result;
}

Здесь, если слово найдено, то возвращаем 1, если нет (в том числе если ранее произошла ошибка), то 0. Это еще больше сокращает и упрощает код. В любом случае между проектировщиком функции и ее потребителем должно быть согласие насчет того, какой именно результат и в каком виде должна возвращать функция.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850