【问题标题】:What is the best way to return an error from a function when I'm already returning a value?当我已经返回一个值时,从函数返回错误的最佳方法是什么?
【发布时间】:2010-09-22 10:52:11
【问题描述】:

我用 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;
}

【问题讨论】:

  • 嘿。 C 中没有多值绑定 :)

标签: c return


【解决方案1】:

有几种方法。各有优缺点。

  • 让函数返回错误代码并传入指向位置的指针以返回结果。这样做的好处是没有结果过载。不好的是你不能直接在表达式中使用函数的真实结果。

    Evan Teran suggested this 的一个变体,它让调用者传递一个指向成功变量的指针(如果调用者不关心,可以选择为 NULL)并从函数返回实际值。这样做的好处是,当调用者可以在错误结果中使用默认值或知道该函数不会失败时,可以直接在表达式中使用该函数。

  • 使用特殊的“哨兵”返回值来指示错误,例如负数(如果正常返回值不能为负)或INT_MAXINT_MIN(如果好的值不能那么极端)。有时要获得更详细的错误信息,需要调用另一个函数(例如GetLastError())或全局变量(例如errno)。这在您的返回值没有无效值时效果不佳,并且通常被许多人认为是错误的形式。

    使用此技术的示例函数是 getc(),如果到达文件末尾或遇到错误,它将返回 EOF。

  • 让函数永远不要直接返回错误指示,而是要求调用者查询另一个函数或全局。这类似于 VB 的“On Error Goto Next”模式的工作原理——而且它几乎被普遍认为是一种糟糕的方式。

  • 另一种方法是使用“默认”值。例如,atoi() 函数与您的 intval() 函数具有几乎相同的功能,当它无法转换任何字符时将返回 0(它与您的函数不同,它消耗字符来转换直到它到达字符串的结尾或不是数字的字符)。

    这里的明显缺点是很难判断实际值是否已转换或垃圾是否已传递给atoi()

    我不太喜欢这种处理错误的方式。

当我想到其他选项时,我会更新...

【讨论】:

  • 我上面的建议是返回结果并将参数作为指向成功变量的指针。如果您不在乎,它允许传递 NULL,但也允许您直接在表达式中使用返回值。
  • 另一个注意事项是 atoi 实际上是标准 c89/c99,因为它被定义为在功能上等同于:strtol(nptr, (char **) NULL, 10);。不过,itoa 绝对是非标准的。
  • @Evan:感谢 atoi() 的更正。我已经编辑了答案以纠正我的错误。
  • 还有一个选项是返回一个复杂的对象(例如数组或哈希映射),其中包含成功/错误状态,如果成功则返回实际结果,如果失败则返回错误标识符。许多非 RESTful JSON API 都会这样做,而忽略 HTTP 状态代码。
  • @EvanTeran 有关atoi ... defined to be functionally equivalent to: strtol() 的详细信息。 1) 返回不同的类型 2) “除了错误行为”表示atoi("xyz");未定义的行为
【解决方案2】:

嗯,.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
}

【讨论】:

  • 说实话,这两种方式都很奇怪 - 但这种方式意味着你可以写 if (intval(text, &result)) { ... }
  • 我认为如果你“知道”它会成功,并且你使用的语言有异常,你应该使用如果失败会显示异常的版本。
  • 灵活性很好,但我更喜欢一致性。我们的编码约定说可能失败的函数将返回状态。 (因为我用 C 编写,所以没有例外)
  • @Korchkidu:哇,所以你不想检查事情是否出错 - 而是继续不管?对我来说,这听起来是个坏主意。
  • @Korchkidu:无论如何我更喜欢检查 - 否则就像你在停车场开车时系上安全带,但在开阔的道路上脱掉它。
【解决方案3】:

一种常见的方法是传递一个指向成功标志的指针,如下所示:

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");
}

【讨论】:

  • 小问题转换代表INT_MIN的字符串,num += string[current_char] - '0';会溢出,就是UB。但通常是可以忍受的 UB。
【解决方案4】:

os 风格的全局 errno 变量也很流行。使用errno.h

如果 errno 不为零,则说明出现问题。

这是errno 的手册页参考。

【讨论】:

  • 但是,如果 errno 为零,这并不一定意味着没有任何问题。标准 C 库并不十分一致地应用 errno。
  • 对。他们不会始终如一地使用它。然而,提问者有机会完全一致。
  • 我认为 errno 并没有受到很多人的喜爱,尽管在 C 标准化时已经有它的先例。例如,参见 P J Plauger 的“标准 C 库”。
  • @Jonathan Leffler:同意,不喜欢 errno。但是问题集中在 C 上,这个问题是 C 语言的标准、众所周知的问题之一。使用 errno 是该问题的众所周知的标准解决方案。
【解决方案5】:

看看标准库是如何处理这个问题的:

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

这里,调用结束后,endptr 指向第一个无法解析的字符。如果 endptr == str,则没有字符被转换,这是一个问题。

【讨论】:

    【解决方案6】:

    总的来说,我更喜欢 Jon Skeet 提出的方式,即。返回一个关于成功的 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;
    }
    

    如果您后来意识到它是一个瓶颈,您仍然可以根据您的需要对其进行优化。

    【讨论】:

      【解决方案7】:

      您可以返回一个类的实例,其中一个属性是感兴趣的值,另一个属性是某种状态标志。或者,传入结果类的一个实例..

      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 绝对是我想说的最多。)

      【讨论】:

        【解决方案8】:

        当我已经返回一个值时,从函数返回错误的最佳方法是什么?

        对各种答案的一些额外想法。


        返回结构

        代码可以返回一个值和一个错误代码。一个问题是类型的扩散。

        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(&amp;dest, x)dest_t foo(x, &amp;error) 的优缺点,

        通过级联使用 _Generic 或函数重载作为编译器扩展,选择 2 种或更多类型,根据调用的参数而不是返回值来区分被调用的底层函数是有意义的。返回普通类型,错误状态。

        示例:一个函数 error_t narrow(destination_t *, source_t) 将一种类型的值转换为更窄的类型,例如 long longshort 并测试源 是否在目标范围内 类型。

        long long ll = ...; 
        int i;
        char ch; 
        error = narrow(&i, ll);
        ...
        error = narrow(&ch, i);
        

        【讨论】:

          猜你喜欢
          • 2010-09-07
          • 1970-01-01
          • 2013-03-30
          • 1970-01-01
          • 1970-01-01
          • 2012-02-03
          • 1970-01-01
          • 2021-05-24
          相关资源
          最近更新 更多