为什么在明显“安全”的代码中使用异常处理?

时间:2009-12-12 23:04:31

标签: delphi try-except

请问,有人可以解释一下,在这段代码中会出现什么异常?

function CreateBibleNames: TStrings;
begin
  Result := TStringList.Create;
  try
    Result.Add('Adam');
    Result.Add('Eva');
    Result.Add('Kain');
    Result.Add('Abel');
  except
    Result.Free;
    raise;
  end;      
end;

由于我使用delphi,我曾经使用过一次异常处理。我认为上面的代码是由熟练的程序员编写的,我不认为异常是多余的。但是,在这个概念中使用异常处理对我来说仍然是一个谜。它似乎是一个安全的代码(没有尝试除了结束)。我已经多次看到类似这样的代码片段,这就是为什么尽管有我的经验,仍然有充分的理由以这种方式编写它,但这并不是必要的。

此外,当某些事情失败时,我会得到异常描述......

Thanx

8 个答案:

答案 0 :(得分:12)

好的,那段代码很奇怪,我同意,而且我完全明白为什么它会这样编写。但它是这样编写的,因为代码背后的前提是错误的。构造看起来很奇怪的事实应该是“代码味道”,并且应该告诉你可能没有尽可能以最好的方式完成某些事情。

首先,这就是为什么try ...中的异常构造除了块。该函数创建一个TStringList,但如果在填充过程中出现问题,那么创建的TStringList将在堆上“丢失”并将成为内存泄漏。所以最初的程序员是防御性的,并确保如果发生异常,将释放TStringList,然后再次引发异常。

现在这里是“坏前提”部分。该函数返回一个TStrings实例。这并不总是最好的方法。返回像这样的对象的实例会引出一个问题“谁将要处理我创建的这个东西?”。它创建了一种情况,在调用方面可能很容易忘记已经分配了TStrings实例。

这里的“更好的实践”是让函数将TStrings作为参数,然后填写现有实例。这样,毫无疑问谁拥有该实例(调用者),从而应该管理其生命周期。

因此,该函数成为一个过程,可能如下所示:

procedure CreateBibleNames(aStrings: TStrings);
begin
  if aStrings <> nil then
  begin
    aStrings .Add('Adam');
    aStrings .Add('Eva');
    aStrings .Add('Kain');
    aStrings .Add('Abel');
  end;      
end;

现在这并不是说每次返回一个对象实例都是一件坏事 - 例如,这是非常有用的Factory模式的唯一功能。但是对于像这样的更“一般”的功能,上面是一种“更好”的做事方式。

答案 1 :(得分:9)

从函数返回新构造的对象时,这是一个很好的习惯。在比字符串列表更复杂的场景中,某些不变量可能会被破坏或发生其他错误,因此会出现异常,在这种情况下,您希望释放返回值。在发生这种情况时,不必检查每种可能的情况,通常一个好的做法是使用一个模式来返回新构造的对象并在任何地方跟随它。

这样,当您构建的对象的实现在将来发生变化时,如果它们碰巧抛出异常,您仍然是安全的。

答案 2 :(得分:4)

我可以看到异常的唯一潜在原因是OutOfMemory异常,如果是这种情况 - 所有投注都是关闭的。在我看来,这是滥用有效概念的一个例子。

过度和不合理地使用try除了使代码混乱并使其更难阅读

答案 3 :(得分:4)

我会用这种方式编写代码,原因如下:

1)通过标准化内存分配的外观,可以轻松检查源代码以查看是否缺少某些内容,而无需读取所有内容。在这里,你只看到.Create,并注意到异常处理是好的,所以你不必阅读try ... except之间的部分。

2)通过标准化编码内存分配的方式,即使在必要时也不会忘记这样做。

3)如果你以后要更改源代码,插入一些代码,更改.Add的工作方式,用其他东西替换TStringList等,那么它就不会破坏这段代码。

4)这意味着您不必考虑TStringList.Add()是否会抛出异常,从而减轻您的大脑以获得更多价值的其他工作。

答案 4 :(得分:2)

最明显的例外是Delphi用于OutOfMemoryException的任何事情。如果没有足够的内存将一些字符串添加到列表中,请释放整个列表并重新抛出异常。

对于制作预定名称列表而言,这似乎是过于防御性的编程 - 通常,如果由于内存不足而导致一个例外,则整个应用程序都会被清除。

答案 5 :(得分:2)

没有TStringList的源代码或权威文档,你真的不知道。仅为了说明的目的,我们假设TStringList被编写为当内存变得紧张时,它开始将列表的部分内容交换到磁盘,从而允许您管理与磁盘一样大的列表。现在调用者也容易受到I / O错误的影响:磁盘空间不足,遇到坏块,网络超时等。

处理此类例外是否过度杀伤?基于场景的判断调用。我们可以向美国宇航局的程序员询问他们的想法。

答案 6 :(得分:2)

这种代码是典型的“工厂”模式。这个例子很简单,可能不需要异常处理,因为它可能会在极端情况下引发异常。 但是如果工厂代码要复杂得多,那么在重新引发任何遇到的异常之前释放之前创建的实例是正确的,因为调用者在异常之前无法知道是否创建了实例。提高。如果返回任何异常,假设未创建或释放实例则要好得多。 当返回的实例可能不总是相同(即给定层次结构中的任何类)时,以及当创建实例的参数对调用者而不是工厂“unknonwn”时,工厂很有用。在这种情况下,调用者无法传递已创建的实例只是为了“填充”适当的参数,但只需要一个实例 - 当然必须清楚调用者成为创建的实例的“所有者”

答案 7 :(得分:-2)

作为一个有趣的附注,Java的一些作者现在认为在几乎所有情况下都要求捕获异常的决定是错误的。这是因为大多数异常确实导致程序被杀死,所以你不妨让运行时系统终止程序并打印堆栈跟踪,而不是强迫程序员捕获异常......好吧,只是打印一些东西并杀死该程序。