计算函数 return 值的最佳实践

Best practice for compute the function return value

我经常用 C 构建函数来检查一些参数和 return 错误代码。

当我发现错误时停止值检查的最佳方法是什么?

第一个例子:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( foo == bar )
  {
     if( foo_1 == bar_1 )
     {
        if( foo_2 == bar_2 )
        {
           error = CHECK_SUCCESS;
        }
     }
  }

  return error;
}

第二个例子:

ErrorCode_e myCheckFunction( some params )
{
  if( foo != bar )
  {
     return CHECK_FAILED;
  }

  if( foo_1 != bar_1 )
  {
     return CHECK_FAILED;
  }

  if( foo_2 != bar_2 )
  {
     return CHECK_SUCCESS;
  }
}

我更喜欢第一种方法,因为我读到 MISRA 规则避免了多个 return 语句。

哪种方法最好?

第二个是最好的,因为它更容易阅读,随着复杂性的增加而扩展得很好,并且在出错时立即停止执行函数。当函数内部有大量错误处理时,这是编写此类函数的唯一明智方法,例如,如果函数是解析器或协议解码器。

MISRA-C 不允许在函数中使用多个 return 语句是 MISRA-C 的缺陷。意图是禁止来自各地的 returns 的意大利面条代码,但教条地禁止多个 return 语句实际上会使代码的可读性大大降低,正如我们从您的示例中看到的那样。想象一下,如果您需要检查 10 个不同的错误。然后你将有 10 个复合 if 语句,这将是一个不可读的混乱。

我已多次向 MISRA 委员会报告此缺陷,但他们没有听取。相反,MISRA-C 只是盲目地引用 IEC 61508 作为规则的来源。这反过来只列出了这个规则的一个有问题的来源(IEC 61508:7 C.2.9),它是一本 1979 年的恐龙编程书。

这既不专业也不科学 - MISRA-C 和 IEC 61508(以及 ISO 26262)应该为(直接或间接)将 1979 年的主观废话列为其唯一来源和理由而感到羞愧。

只需使用第二种形式并针对此缺陷 MISRA 规则提出永久偏差。

我使用的方法是转到error_exit。

您必须考虑功能可能失败的原因。

原因 1 是非法参数,例如将负数传递给平方根。所以断言失败,错误是调用者的。

原因 2 内存不足 - 这是可伸缩函数的固有问题。您需要分流故障,但通常如果程序不会给您少量内存来保存文件路径,那么它就死了。

原因 3 是语法错误。这是非法参数的特例。如果参数是平方根的双精度数,则可以合理地期望调用者检查负数。如果参数是一个基本程序,则调用者无法检查正确性,除非有效地编写他自己的解析器。所以糟糕的语法需要作为正常的流程控制来处理。

原因 4 是硬件故障。除非您熟悉特定设备,否则除了分流错误外,您无能为力。

原因 5 是内部程序错误。根据定义,没有正确的行为,因为您自己的代码不正确。但是,例如,您经常需要捏造或丢弃几何中的退化情况。

然而,goto error_exit 方法是我喜欢的方法。它保持一个入口点。并且退出原则基本上完好无损,没有为内存分配错误引入人工嵌套,这些错误发生的可能性比计算机故障更小。

我倾向于混合使用两种样式,之前使用第二种样式(多个 returns),并且(可能)使用第一种样式(稍后 returned 的局部变量)之后。

理由是:"multiple returns" 是 确定的 。它 can/should 在传递的参数绝对错误或其他一些不可恢复的情况下使用。
相反,"local variable" 样式允许编写可以多次修改 return 值的代码。它倾向于生成表示 "let's start by supposing failure; but if everything is ok, then I will rewrite the result as OK" 的代码。或者相反:"assume OK; if anything goes wrong set the result as failure"。在这些步骤之间,仍然可以有其他 returns!

作为最后的想法......我会说正确的风格取决于情况,永远不要假设一个总是对的而另一个总是错的。

我同意 但我想提供另一种解决方案,该解决方案符合单一退出规则,并且仍然与第二个示例具有类似的可读性:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( foo != bar )
  {
     error = CHECK_FAILED;
  }
  else if( foo_1 != bar_1 )
  {
     error = CHECK_FAILED;
  }
  else if( foo_2 != bar_2 )
  {
     error = CHECK_SUCCESS;
  }
  else
  {
     // else (even empty) is required by MISRA after else-if
  }
  return error;
}

由于示例中只有两个选项,我们可以只使用一个条件:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( (foo == bar) && (foo_1 == bar_1) && (foo_2 != bar_2) )
  {
     error = CHECK_SUCCESS;
  }

  return error;
}

这种情况可以更简化,我们不需要任何局部变量:

ErrorCode_e myCheckFunction( some params )
{
  return ( (foo == bar) && (foo_1 == bar_1) && (foo_2 != bar_2) )
      ? CHECK_SUCCESS : CHECK_FAILED;
}

有趣的是没有人注意到,上面的第二个例子演示了为什么首先存在 MISRA 规则:它为所有 if 子句不匹配的情况留下了默认 return 值。

那么如果 (foo == bar) && (foo1 == bar1) && (foo2 == bar2) 会发生什么?

此外,第一个例子对我来说更容易理解,在这种特殊情况下有一个 non-default return 值。