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

Posted

技术标签:

【中文标题】当我已经返回一个值时,从函数返回错误的最佳方法是啥?【英文标题】: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 中没有多值绑定 :) 【参考方案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);

【讨论】:

以上是关于当我已经返回一个值时,从函数返回错误的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

从函数返回多个值的最佳方法是啥?

当为“var”和“let”分配一个引发错误的函数的返回值时,是啥导致了它们之间的不同行为

当我尝试从 PHP 对象/数组返回值时,出现 500 错误。但是可以返回所有对象

从 Oracle 函数返回记录的标准方法是啥?

在函数结束(例如检查失败)之前在 python 中退出函数(没有返回值)的最佳方法是啥?

从 Rails Helper 返回多个标签的最佳方法是啥?