7

这个问题可能与语言无关,但我将专注于指定的语言。

在处理一些遗留代码时,我经常看到函数的示例,这些函数(在我看来,显然)在它们内部做了太多的工作。我说的不是 5000 个 LoC 怪物,而是关于在它们内部实现先决条件检查的函数。

这是一个小例子:

void WorriedFunction(...) {
   // Of course, this is a bit exaggerated, but I guess this helps
   // to understand the idea.
   if (argument1 != null) return;
   if (argument2 + argument3 < 0) return;
   if (stateManager.currentlyDrawing()) return;

   // Actual function implementation starts here.

   // do_what_the_function_is_used_for
}

现在,当调用这种函数时,调用者不必担心要满足的所有先决条件,可以简单地说:

// Call the function.
WorriedFunction(...);

现在 -应该如何处理以下问题?

就像,一般来说 - 这个函数是否应该只做它所要求的并将“先决条件检查”移动到调用方:

if (argument1 != null && argument2 + argument3 < 0 && ...) {
   // Now all the checks inside can be removed.
   NotWorriedFunction();
}

或者 - 它是否应该在每个先决条件不匹配时简单地抛出异常?

if (argument1 != null) throw NullArgumentException;

我不确定这个问题是否可以一概而论,但我仍然想在这里谈谈你对此的看法——也许我可以重新考虑一些事情。

如果您有其他解决方案,请随时告诉我:)

谢谢你。

4

5 回答 5

6

每个函数/方法/代码块都应该有一个precondition,这是它设计工作的精确环境,还有一个postcondition,即函数返回时的世界状态。这些可以帮助您的程序员同事了解您的意图。

根据定义,如果前置条件为假,则代码不会工作,如果后置条件为假,则认为代码有问题。

无论你是把这些写在你的脑海里、写在设计文档的纸上、在评论中,还是在实际的代码检查中,这都是一种品味问题。

但是,如果您将前置条件和后置条件编码为显式检查,那么长期的实际问题会更容易。如果你编写这样的检查代码,因为代码不应该在错误的前置条件下工作,或者有错误的后置条件,前置条件和后置条件检查应该导致程序以一种简单的方式报告错误发现故障点。正如您的示例所示,代码不应该做的只是“返回”什么都不做,因为这意味着它已经以某种方式正确执行。(代码当然可以定义为什么都不做就退出,但如果是这种情况,前置条件和后置条件应该反映这一点。)

您显然可以使用 if 语句编写此类检查(您的示例非常接近):

if (!precondition) die("Precondition failure in WorriedFunction"); // die never comes back

但是通常通过为称为断言的语言定义一个特殊的函数/宏/语句来在代码中指示前置或后置条件的存在,并且这种特殊构造通常会在断言时导致程序中止和回溯是假的。

代码应该是这样写的:

void WorriedFunction(...)  
 {    assert(argument1 != null); // fail/abort if false [I think your example had the test backwards]
      assert(argument2 + argument3 >= 0);
      assert(!stateManager.currentlyDrawing());
      /* body of function goes here */ 
 }  

复杂的函数可能愿意告诉其调用者某些条件已失败。这就是异常的真正目的。如果存在异常,从技术上讲,后置条件应该说明“函数可能在 xyz 条件下异常退出”。

于 2011-03-15T22:56:43.663 回答
3

这是一个有趣的问题。检查“按合同设计”的概念,您可能会发现它很有帮助。

于 2011-03-15T22:33:41.943 回答
3

这取决于。

我想在案例 1、3 和案例 2 之间分开我的答案。

案例一、三

如果您可以安全地从参数问题中恢复,请不要抛出异常。一个很好的例子是TryParse方法 - 如果输入格式错误,它们只是返回false。另一个示例(例外)是所有 LINQ 方法 - 如果sourceis null或其中一个强制Func<>是,它们会抛出null。但是,如果他们接受自定义IEqualityComparer<T>or IComparer<T>,他们不会抛出,而只是使用EqualityComparer<T>.Defaultor的默认实现Comparer<T>.Default。这完全取决于参数的使用上下文以及您是否可以安全地从中恢复。

案例2

如果代码位于类似环境的基础设施中,我只会使用这种方式。最近,我开始重新实现 LINQ 堆栈,您必须实现几个接口 - 这些实现永远不会在我自己的类和方法之外使用,因此您可以在它们内部进行假设 - 外部将始终通过接口访问它们和不能自己创建它们。

如果您对 API 方法做出这样的假设,您的代码将在错误输入时抛出各种异常,并且用户不知道发生了什么,因为他不知道您的方法的内部。

于 2011-03-15T22:37:52.367 回答
2

“或者——它应该在每个先决条件不匹配时简单地抛出异常吗?”

是的。

于 2011-03-15T22:33:33.677 回答
1

您应该在调用和函数之前进行检查,如果您拥有该函数,如果传递的参数不符合预期,您应该让它抛出异常。

在您的调用代码中,应处理这些异常。当然,传递的参数应该在调用之前进行验证。

于 2011-03-15T22:37:17.343 回答