6

拜托,有人可以解释一下,什么会在这段代码中引发异常?

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

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

谢谢

4

8 回答 8

12

好吧,那段代码很奇怪,我同意,而且我完全理解为什么要这样写。但它是这样写的,因为代码的前提是错误的。构造看起来很奇怪的事实应该是“代码气味”,并且应该告诉您某些事情可能没有以最好的方式完成。

首先,这就是为什么 try...except 块中的不寻常构造。该函数创建了一个 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;

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

于 2009-12-13T05:23:31.893 回答
9

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

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

于 2009-12-13T05:15:07.117 回答
4

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

过度且不合理地使用 try except 会使代码混乱并使其更难阅读

于 2009-12-12T23:13:23.390 回答
4

我会这样写代码,有几个原因:

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

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

3)如果您稍后更改源代码,插入一些代码,更改 .Add 的工作方式,将 TStringList 替换为其他内容等,那么它不会破坏此代码。

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

于 2009-12-13T09:08:45.937 回答
2

最明显的例外是 Delphi 对OutOfMemoryException. 如果没有足够的内存将几个字符串添加到列表中,请释放整个列表并重新抛出异常。

这似乎是用于制作预定名称列表的过度防御性编程 - 通常,如果由于内存不足而出现异常,则整个应用程序都会被淹没。

于 2009-12-12T23:07:24.310 回答
2

如果没有 TStringList 的来源或权威文档,您将无法真正知道。仅出于说明目的,让我们假设 TStringList 的编写方式是,当内存紧张时,它会开始在磁盘之间交换部分列表,从而允许您管理与磁盘一样大的列表。现在调用者也容易受到 I/O 错误的影响:磁盘空间不足、遇到坏块、网络超时等。

处理此类异常是否过大?基于场景的判断调用。我们可以询问 NASA 的程序员他们的想法。

于 2009-12-13T03:21:39.297 回答
2

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

于 2009-12-13T14:40:03.863 回答
-2

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

于 2009-12-12T23:16:40.590 回答