基于参数抛出/不抛出异常——为什么这不是一个好主意?

8

我在 MSDN 上搜索,发现了 这篇文章,其中有一个有趣的建议:不要有公共成员可以基于某些选项抛出或不抛出异常。

例如:

Uri ParseUri(string uriValue, bool throwOnError)

当然,我可以看到在99%的情况下这是可怕的,但它的偶尔使用是否合理?

我见过一个案例,就是在访问数据库或配置文件中的数据时使用“AllowEmpty”参数。例如:

object LoadConfigSetting(string key, bool allowEmpty);

在这种情况下,另一种选择是返回null。但是,此时调用代码将会充斥着对null引用的检查。(如果你想使用null作为特定配置值,该方法也会排除此能力)。
你有什么想法?为什么这会成为一个大问题?
8个回答

10

我认为基于布尔值来进行抛出或不抛出异常的决定肯定是个糟糕的想法。尤其是因为它要求开发者在查看代码时具备对API的功能性知识才能确定布尔值的含义。这本身就是不好的,但当它改变底层的错误处理时,读取代码时很容易让开发者犯错。

在这种情况下,有两个API会更好且更易读。

Uri ParseUriOrThrow(string value);

bool TryParseUri(string value, out Uri uri);
在这种情况下,这些API的作用是100%清楚的。
布尔值作为参数的原因:http://blogs.msdn.com/jaredpar/archive/2007/01/23/boolean-parameters.aspx

3

通常最好选择一种错误处理机制并始终坚持使用。允许这种翻转代码的存在实际上无法改善开发人员的生活。

在上面的示例中,如果解析失败且throwOnError为false会发生什么?现在用户必须猜测是否将返回NULL,或者谁知道...

确实,异常和返回值作为更好的错误处理方法之间存在持续的辩论,但我相信有一个关于保持一致并坚持您所做选择的共识。API不能使其用户感到惊讶,错误处理应该是接口的一部分,并且应该像接口一样明确定义。


1

从可读性的角度来看,这有点令人不爽。开发人员倾向于期望每个方法都会抛出异常,如果他们想忽略异常,他们会自己捕获它。使用“布尔标志”方法,每个方法都需要实现这种抑制异常的语义。

然而,我认为MSDN文章严格指的是“throwOnError”标志。在这些情况下,错误要么在方法内部被忽略(不好,因为它被隐藏了),要么返回某种空/错误对象(不好,因为您没有使用异常来处理错误,这是不一致和容易出错的)。

而你的例子对我来说很好。异常表示方法未能执行其职责 - 没有返回值。但是,“allowEmpty”标志改变了方法的语义 - 所以本来应该是异常(“空值”)现在是预期和合法的。此外,如果您已经抛出了异常,您将无法轻松地返回配置数据。所以在这种情况下似乎没问题。


1
在任何公共API中,有两种检查错误条件的方式真的是一个很糟糕的想法,因为这样就不明显如果发生错误会发生什么。仅仅通过查看代码是无法帮助你的。您必须理解标志参数的语义(没有任何东西阻止它成为表达式)。
如果检查null不是一个选项,并且如果我需要从这个特定的失败中恢复,我更喜欢创建一个特定的异常,以便稍后可以捕获它并适当地处理它。在任何其他情况下,我都会抛出一个通用异常。

0

拥有一个不抛出异常的参数会破坏异常的整个意义(在任何语言中都是如此)。如果调用代码想要:

public static void Main()
{
        FileStream myFile = File.Open("NonExistent.txt", FileMode.Open, FileAccess.Read);
}

欢迎使用(C#甚至没有检查异常)。在Java中,可以通过以下方式实现:

public static void main(String[] args) throws FileNotFoundException
{
        FileInputStream fs = new FileInputStream("NonExistent.txt");
}

无论哪种方式,处理(或不处理)异常的工作都由调用方决定,而不是被调用方。

0

与此类似的另一个例子可能是一些值类型上的TryParse方法集合

bool DateTime.TryParse(string text, out DateTime)


0
在链接的文章中提到,异常不应该用于流程控制,这似乎是示例问题中所暗示的。异常应该反映方法级别的失败。拥有一个签名表明可以抛出错误似乎意味着设计没有经过深思熟虑。
Jeffrey Richter的书《CLR via C#》指出:“当方法无法完成其名称所指示的任务时,应该抛出异常。”
他的书还指出了一个非常常见的错误。人们倾向于编写捕获所有异常的代码(他的话是“开发人员普遍犯的一个错误是没有接受适当的异常使用培训,往往会过度使用和不正确地使用catch块。当你捕获异常时,你表明你预期了这个异常,你理解它为什么发生,并且你知道如何处理它。”)
这使我尝试编写可以预期并且可以在我的逻辑中处理的异常,否则就应该是一个错误。
验证您的参数并防止异常,只捕获您可以处理的异常。

我认为你误解了情境。我们有一个选项,要么抛出异常(这将停止程序或其他操作),要么不抛出异常(即返回null或什么都不做)。在任何情况下,都不会抛出异常然后捕获它们。 - cbp

0
我认为,有一个参数指示失败是否应该引发异常或仅返回错误指示通常是有用的,因为这样的参数可以轻松地从外部程序传递到内部程序。考虑以下内容:
Byte[] ReadPacket(bool DontThrowIfNone) // 如果没有,则记录为返回null { int len = ReadByte(DontThrowIfNone); // 如果没有,则记录为返回-1 if (len < 0) return null; Byte[] packet = new Byte[len]; if (ReadMultiBytes(packet, 0, len, true) != len) throw new ApplicationException("Unexpected end of stream"); return packet; }
如果读取数据时出现TimeoutException之类的问题,应该在ReadByte()或ReadMultiBytesbytes()中抛出此类异常。但是,如果缺少数据被视为正常情况,则ReadByte()或ReadMultiBytesbytes()例程不应引发异常。如果只使用do/try模式,则ReadPacket和TryReadPacket例程需要具有几乎相同的代码,但其中一个使用Read*方法,另一个使用TryRead*方法。很糟糕。
最好使用枚举而不是布尔值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接