当我已经返回一个值时,从函数返回错误的最佳方法是啥?
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_MAX
或INT_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(&dest, x)
与dest_t foo(x, &error)
的优缺点,
通过级联使用 _Generic
或函数重载作为编译器扩展,选择 2 种或更多类型,根据调用的参数而不是返回值来区分被调用的底层函数是有意义的。返回普通类型,错误状态。
示例:一个函数 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);
【讨论】:
以上是关于当我已经返回一个值时,从函数返回错误的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章
当为“var”和“let”分配一个引发错误的函数的返回值时,是啥导致了它们之间的不同行为
当我尝试从 PHP 对象/数组返回值时,出现 500 错误。但是可以返回所有对象