计算函数返回值的最佳实践

Posted

技术标签:

【中文标题】计算函数返回值的最佳实践【英文标题】:Best practice for compute the function return value 【发布时间】:2017-07-23 08:29:56 【问题描述】:

我经常用 C 语言构建函数来检查一些参数并返回错误代码。

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

第一个例子:

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 规则避免使用多个返回语句。

哪种方法最好?

【问题讨论】:

如果你更喜欢第一个,那是最好的(对你来说):) 这实际上不是一个基于意见的问题。 OP 更喜欢一个版本,因为 MISRA-C 告诉他这样做,没有说明理由。这里真正的问题是为什么 MISRA-C 会做出这样的声明。过去,我已经深入了解了为什么这个规则甚至存在,请在下面回答。 Pattern to prevent continually checking error?的可能重复 @Toby 不是重复的,因为它没有解决 MISRA 方面的问题。 @Lundin 没有明确提到 MISRA,但在问题中我表达了对单个退出点的偏好(IIRC 无论如何都是因为 MISRA 要求)和答案提供的例子都说明了这一点。 WRT 实际上回答了所提出的问题(而不是对 MISRA 发表意见)我认为那里的答案也回答了这个问题。 【参考方案1】:

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

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

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

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

只需使用第二种形式,就可以永久偏离此缺陷 MISRA 规则。

【讨论】:

我同意你的看法。当需要检查多个参数时,第二种方法是最好的。特别是为了代码的可读性。 @Andrew 缺陷 = 有问题的来源。话虽如此,任何 C 程序员都很清楚,上述问题中的第二版比第一版更具可读性。尤其是当错误检查的数量越来越多时。这里实际上有 3 种选择:1)嵌套复合语句疯狂,2)错误返回,或 3)“错误转到”à la BASIC。第一个是不可读且难以维护的,第二个是非常可读且坚固的代码,第三个是可以接受的,但不如第二个可读。 MISRA-C:2004 仅允许 1)。 MISRA-C:2012 仅允许 1) 和 3)。它怎么不是缺陷? 规则 15.5 是 建议性 - 如果你能证明这个决定是合理的,就不要应用它 :-) 是的......它被重新分类为:2012 :-) 我认为更好的规则是说一个函数可能有任意数量的提前退出返回,这些返回发生在第一个副作用之前,并且可能在最后一个副作用之后有多个返回导致副作用的语句,或导致副作用本身的单个返回。我认为应该允许最合理的用例,同时禁止最不合理的用例。【参考方案2】:

我同意Lundin’s answer,但我想提供另一种符合单一退出规则的解决方案,并且仍然与第二个示例类似:

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;

【讨论】:

满足要求、可扩展和可维护的代码示例!【参考方案3】:

我使用的方法是goto error_exit。

你必须考虑一个函数为什么会失败。

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

原因 2 内存不足 - 这是可扩展函数的固有问题。您需要将故障排除在外,但通常如果程序不会为您提供少量内存来保存文件路径,那么它就死了。

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

原因 4 是硬件故障。除非您熟悉特定的设备,否则您无能为力,只能避免错误。

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

不过,goto error_exit 方法是我最喜欢的方法。它保留了一个入口点。并且退出原则基本完好,没有为内存分配错误引入人工嵌套,这种错误发生的可能性比计算机崩溃的可能性要小。

【讨论】:

在我看来,比“on error goto”更好的版本是让函数成为实际函数的包装器。将所有资源分配/释放留在包装器中。它本质上与“on error goto”相同,但更面向对象,将分配与算法分开。作为奖励,您不必忍受无休止的“goto 被认为有害”辩论。 另一个答案是将malloc包装在xmalloc中,这样可以保证不会失败。如果失败,它会弹出一个对话框,要求用户释放一些内存或终止。 虽然“goto fail”始终是一个选项(尽管 MISRA C 的早期版本需要一个偏差,但请记住,Apple 使用该方法遇到了一个相当尴尬的 SSL 错误!【参考方案4】:

我倾向于混合使用这两种样式,之前使用第二种样式(多次返回),之后(可能)使用第一种样式(稍后返回的局部变量)。

理由是:“多重回报”是确定性的。当传递的参数有绝对错误或其他不可恢复的情况时,可以/应该使用它。 相反,“局部变量”样式允许编写可以多次修改返回值的代码。它产生的代码往往意味着“让我们从假设失败开始;但如果一切正常,那么我会将结果重写为 OK”。或者相反:“假设 OK;如果出现任何问题,则将结果设置为 failure”。而在这些步骤之间,还可以有其他的回报!

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

【讨论】:

【参考方案5】:

有趣的是,没有人注意到,上面的第二个例子证明了 MISRA 规则首先存在的原因:它为 if 子句不匹配的所有情况留下了默认返回值。

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

此外,在第一个示例中,对我来说更容易掌握,在这种特殊情况下,有一个非默认返回值。

【讨论】:

以上是关于计算函数返回值的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

JDK 8 函数式编程最佳实践

JDK 8 函数式编程最佳实践

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

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

返回多个值的Java方法的最佳实践? [复制]

不使用返回的函数值是不是会影响内存?