Каков наилучший способ вернуть ошибку из функции, когда я уже возвращаю значение? - PullRequest
28 голосов
/ 15 ноября 2008

Я написал функцию в C, которая преобразует строку в целое число и возвращает целое число. Когда я вызываю функцию, я также хочу, чтобы она дала мне знать, если строка не является допустимым числом. В прошлом я возвращал -1, когда произошла эта ошибка, потому что мне не нужно было преобразовывать строки в отрицательные числа. Но теперь я хочу, чтобы он преобразовывал строки в отрицательные числа, так как лучше всего сообщить об ошибке?

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

Вот код:

s32 intval(const char *string) {
    bool negative = false;
    u32 current_char = 0;

    if (string[0] == '-') {
        negative = true;
        current_char = 1;
    }

    s32 num = 0;
    while (string[current_char]) {
        if (string[current_char] < '0' || string[current_char] > '9') {
            // Return an error here.. but how?
        }

        num *= 10;
        num += string[current_char] - '0';
        current_char++;
    }

    if (negative) {
        num = -num;
    }

    return num;
}

Ответы [ 8 ]

35 голосов
/ 15 ноября 2008

Есть несколько способов. У всех есть свои плюсы и минусы.

  • Пусть функция вернет код ошибки и передаст указатель на местоположение, чтобы вернуть результат. Хорошая вещь об этом - нет перегрузки результата. Плохо то, что вы не можете использовать реальный результат функции непосредственно в выражении.

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

  • Используйте специальное возвращаемое значение 'sentinel', чтобы указать ошибку, например, отрицательное число (если нормальные возвращаемые значения не могут быть отрицательными) или INT_MAX или INT_MIN, если хорошие значения не могут быть такими экстремальными. Иногда для получения более подробной информации об ошибке необходимо обратиться к другой функции (такой как GetLastError()) или к глобальной переменной (такой как errno). Это не работает, когда ваше возвращаемое значение не имеет недопустимых значений, и многие люди считают его плохой формой.

    Примером функции, которая использует эту технику, является getc (), которая возвращает EOF, если достигнут конец файла или обнаружена ошибка.

  • Пусть функция никогда не возвращает указание об ошибке напрямую, а требует, чтобы вызывающий абонент запросил другую функцию или глобальную. Это похоже на то, как работает режим "On Error Goto Next" в VB - и это почти всегда считается плохим путем.

  • Еще один способ - использовать значение по умолчанию. Например, функция atoi(), которая имеет почти те же функции, что и ваша функция intval(), вернет 0, когда она не может преобразовать какие-либо символы (она отличается от вашей функции тем, что использует символы для преобразования, пока не достигнет конец строки или символ, который не является цифрой).

    Очевидным недостатком здесь является то, что может быть сложно определить, было ли преобразовано фактическое значение или был передан мусор в atoi().

    Я не большой поклонник этого способа обработки ошибок.

Я буду обновлять, как другие варианты приходят мне в голову ...

16 голосов
/ 15 ноября 2008

Что ж, способ, которым .NET обрабатывает это в Int32.TryParse , - это возвращать успех / сбой и передавать проанализированное значение обратно с параметром передачи по ссылке. То же самое можно применить в C:

int intval(const char *string, s32 *parsed)
{
    *parsed = 0; // So that if we return an error, the value is well-defined

    // Normal code, returning error codes if necessary
    // ...

    *parsed = num;
    return SUCCESS; // Or whatever
}
11 голосов
/ 15 ноября 2008

Обычный способ - передать указатель на флаг успеха следующим образом:

int my_function(int *ok) {
    /* whatever */
    if(ok) {
        *ok = success;
    }
    return ret_val;
}

Назовите это так:

int ok;
int ret = my_function(&ok);
if(ok) {
    /* use ret safely here */
}

РЕДАКТИРОВАТЬ: пример реализации здесь:

s32 intval(const char *string, int *ok) {
    bool negative = false;
    u32 current_char = 0;

    if (string[0] == '-') {
        negative = true;
        current_char = 1;
    }

    s32 num = 0;
    while (string[current_char]) {
        if (string[current_char] < '0' || string[current_char] > '9') {
                // Return an error here.. but how?
                if(ok) { *ok = 0; }
        }

        num *= 10;
        num += string[current_char] - '0';
        current_char++;
    }

    if (negative) {
        num = -num;
    }
    if(ok) { *ok = 1; }
    return num;
}

int ok;
s32 val = intval("123a", &ok);
if(ok) {
    printf("conversion successful\n");
}
6 голосов
/ 15 ноября 2008

Глобальная переменная errno в стиле os также популярна. Используйте errno.h.

Если значение errno не равно нулю, значит, что-то пошло не так.

Вот ссылка на справочную страницу для errno .

4 голосов
/ 17 ноября 2008

Посмотрите, как стандартная библиотека решает эту проблему:

long  strtol(const  char  * restrict str,  char **restrict endptr, int base);

Здесь после вызова endptr указывает на первый символ, который не может быть проанализирован. Если endptr == str, то символы не были преобразованы, и это проблема.

3 голосов
/ 15 ноября 2008

В общем, я предпочитаю то, что предложил Джон Скит, т.е. возвращая bool (int или uint) об успехе и сохраняя результат в переданном адресе. Но ваша функция очень похожа на strtol, поэтому я считаю хорошей идеей использовать тот же (или похожий) API для вашей функции. Если вы дадите ему имя, похожее на my_strtos32, это поможет вам понять, что делает функция, не читая документацию.

РЕДАКТИРОВАТЬ: Поскольку ваша функция явно основана на 10, my_strtos32_base10 является лучшим именем. Пока ваша функция не является узким местом, вы можете пропустить реализацию. И просто обернуть вокруг strtol:


s32
my_strtos32_base10(const char *nptr, char **endptr)
{
    long ret;
    ret = strtol(nptr, endptr, 10);
    return ret;
}

Если позже вы поймете, что это узкое место, вы все равно можете оптимизировать его для своих нужд.

0 голосов
/ 14 июля 2018

Каков наилучший способ вернуть ошибку функции, когда я уже возвращаю значение?

Некоторые дополнительные мысли к различным ответам.


Вернуть структуру

Код может возвращать значение и код ошибки. Беспокойство вызывает распространение типов.

typedef struct {
  int value;
  int error;
} int_error;

int_error intval(const char *string);

...

int_error = intval(some_string);
if (int_error.error) {
  Process_Error();
}

int only_care_about_value = intval(some_string).value;
int only_care_about_error = intval(some_string).error;

Не число и NULL

Использовать специальное значение, когда тип возврата функции предоставляет его.
Номера C не требуются, но вездесущи.

#include <math.h>
#include <stddef.h>

double y = foo(x);
if (isnan(y)) {
  Process_Error();
}

void *ptr = bar(x);
if (ptr == NULL) {
  Process_Error();
}

_Generic / Перегрузка функций

Учитывая плюсы и минусы error_t foo(&dest, x) против dest_t foo(x, &error),

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

Пример: функция error_t narrow(destination_t *, source_t), которая преобразует значение одного типа в более узкий тип, например long long в short, и проверяет, находится ли исходное значение в диапазоне цели тип .

long long ll = ...; 
int i;
char ch; 
error = narrow(&i, ll);
...
error = narrow(&ch, i);
0 голосов
/ 15 ноября 2008

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

Pseudo code
  MyErrStatEnum = (myUndefined, myOK, myNegativeVal, myWhatever)

ResultClass
  Value:Integer;
  ErrorStatus:MyErrStatEnum

Пример 1:

result := yourMethod(inputString)

if Result.ErrorStatus = myOK then 
   use Result.Value
else
  do something with Result.ErrorStatus

free result

Пример 2

create result
yourMethod(inputString, result)

if Result.ErrorStatus = myOK then 
   use Result.Value
else
  do something with Result.ErrorStatus

free result

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

Для дальнейшего расширения этой концепции она также применяется к вызовам методов с несколькими входными параметрами. Например, вместо CallYourMethod (val1, val2, val3, bool1, bool2, string1) вместо этого используйте класс со свойствами, соответствующими val1, val2, val3, bool1, bool2, string1, и используйте его в качестве единственного входного параметра. Он очищает вызовы методов и делает код более легким в будущем. Я уверен, что вы видели, что вызовы методов с более чем несколькими параметрами гораздо сложнее использовать / отлаживать. (7 - это самое большее, что я бы сказал.)

...