Иногда необходимо, чтобы функция уведомляла внешний контекст о результатах своей работы на различных ее этапах. В этом случае распространенным решением является возврат из функции статусных кодов. Получив из функции статусный код,
вызывающий код может его обработать по своему усмотрению. В качестве статусного кода результата часто используются числовые значения. Например, в 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. Это еще больше сокращает и упрощает код. В любом случае между проектировщиком функции и ее потребителем должно быть согласие насчет того, какой именно результат и в каком виде должна возвращать функция.