.NET Framework中的一个常见模式是TryXXX模式(我不知道这是否就是他们真正称之为的模式),其中被调用方法尝试执行某些操作,如果成功则返回True
,如果操作失败,则为False
。一个很好的例子是通用Dictionary.TryGetValue
方法。
这些方法的文档说它们不会抛出异常:该失败将在方法返回值中报告。到目前为止一切都很好。
我最近遇到了两种不同的情况,其中实现TryXXX模式的.NET Framework方法引发了异常。有关详细信息,请参阅Bug in System.Random constructor?和Uri.TryCreate throws UriFormatException?。
在这两种情况下,TryXXX
方法都会调用其他方法来引发意外异常,并且这些异常会被转义。我的问题:这是否打破了不抛出异常的隐含合同?
换句话说,如果您正在撰写TryFoo
,您是否可以通过这样写来保证异常无法逃脱?
public bool TryFoo(string param, out FooThing foo)
{
try
{
// do whatever
return true;
}
catch
{
foo = null;
return false;
}
}
这很诱人,因为这可以保证不会逃避任何例外,从而兑现隐含的合同。但这是一个隐藏的bug。
根据我的经验,我的直觉是,这是一个非常糟糕的主意。但是如果TryFoo
让一些异常逃脱,那么它真正说的是“我不会抛出任何我知道如何处理的异常”,然后是合同的整个想法“我不会抛出异常“被扔出窗外。
那么,你有什么看法? TryFoo
应该处理所有异常,还是只处理它预期发生的异常?你的理由是什么?
答案 0 :(得分:5)
这取决于。 TryParseFoo
应该捕获所有解析异常,例如null或格式错误的字符串。但也有不相关的例外情况(例如ThreadAbortException
),不应该自动安静。
在Python中(跟我一起),你可以通过捕获KeyboardInterrupt
异常来解决很多问题,这基本上意味着你的程序即使你用Ctrl-C也能继续运行。
我的经验法则是,如果您想要TryFoo
而不清理输入,则应使用Foo
。如果TryFoo
发现与Foo
输入无关的错误,则会过于激进。
答案 1 :(得分:5)
不保证不抛出异常。在Dictionary上考虑这个简单的例子......
var dic = new Dictionary<string, string>() { { "Foo", "Bar" } };
string val = String.Empty;
string key = null;
dic.TryGetValue(key, out val); // oops
我发送了一个空键,我得到一个NullArgumentException。反射器因此显示代码...
private int FindEntry(TKey key)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
// more stuff
}
在我看来,合同的意思是“如果你试图做的事情失败了,我就不会抛出异常”,但这远非“我不会抛出任何例外”。既然你可能没有编写框架,你可能会妥协这样......
catch( Exception ex )
{
Logger.Log( ex );
Debug.Assert( false );
foo = null;
return false;
}
...并且不处理具有该catch块的TryXXX失败案例(因此您没有一堆简单的日志条目)。在这种情况下,您至少会揭示错误的性质并在开发期间识别它。时间以及在运行时记下它。
答案 2 :(得分:3)
我认为TryFoo
不得抛出异常。我认为TryFoo
的合同是它将处理无效输入作为正常情况,而不是例外情况。也就是说,如果由于输入无效而导致任务失败,则不会抛出异常;如果由于任何其他原因失败,它应该抛出异常。我在TryFoo
返回后的期望是它处理了我的输入,或者我的输入无效。如果这些都不是真的,我想要一个例外,所以我知道它。
答案 3 :(得分:2)
您链接的另外两个StackOverflow问题似乎是真正的错误。例如Uri.TryCreate抛出UriFormatException的那个肯定是合同的中断,因为该方法的MSDN文档明确指出:
如果无法创建Uri,则不会抛出异常。
另一个,TryDequeue,我找不到任何文档,但它似乎也是一个bug。如果这些方法被称为 TryXXX ,但是文档明确声明它们可能会抛出异常,那么您可能会有一个观点。
通常会有这种方法的两个版本,一个抛出异常,另一个抛出异常。例如,Int32.Parse和Int32.TryParse。 TryXXX 方法的要点不是隐藏异常。这适用于失败实际上并非特殊情况的情况。例如,如果您正在从控制台读取数字并希望继续尝试直到输入正确的数字,则您不希望继续捕获异常。同样,如果你想从字典中获取一个值,但它可能不存在是完全正常的情况,你可以使用TryGetValue。
摘要: TryXXX 不应抛出异常,也不会隐藏异常。它适用于失败的正常和非异常情况,并且您希望轻松地检测到这种情况,而无需捕获异常的开销和精力。通常情况下,提供异常的非TryXXX方法也适用于例外情况。
答案 4 :(得分:1)
TryXXX
背后的想法是它不会抛出通常在您调用相应的XXX
调用时通常会发生的特定类型的异常 - 该方法的使用者知道他们忽略任何通常会发生的异常。
这很诱人,因为这样可以保证不会逃避任何例外,从而兑现隐含的合同。
这不完全正确 - 可以逃避try/catch
(例如OutOfMemoryException
,StackOverflowException
等)的一些例外情况。
答案 5 :(得分:1)
你几乎不应该抓住所有例外。如果您必须使用围绕XXX的TryXXX
对(而不是首选,XXX根据TryXXX实施并投掷错误结果)实施try/catch
,请捕获特定的预期异常(s)only。
如果存在语义上不同的问题,TryXXX
可以抛出,例如程序员错误(可能在不期望的地方传递null)。
答案 6 :(得分:1)
这取决于方法尝试做什么。
如果方法例如从数据读取器读取字符串值并尝试将其解析为整数,则在解析失败时不应抛出异常,但如果数据读取器无法读取,则应抛出异常数据库或者您正在阅读的值根本不是字符串。
该方法应遵循捕获异常的一般原则,您应该只捕获您知道如何处理的异常。如果发生了一些完全不同的异常,你应该让它泡到其他知道如何处理它的代码。
答案 7 :(得分:1)
换句话说,如果您正在编写TryFoo,您能保证吗? 异常无法逃脱 这样写吗?
不,我不会在我的TryFoo方法中吞下异常。 Foo依赖于TryFoo,而不是相反。既然你提到了通用字典,它有一个GetValue和一个TryGetValue方法,我会按如下方式编写我的字典方法:
public bool TryGetValue(T key, out U value)
{
IList<KeyValue> kvCollection = internalArray[key.GetHashCode() % internalArray.Length];
for(KeyValue kv in kvCollection)
{
if(kv.Key == key)
{
value = kv.Value;
return true;
}
}
value = default(U);
return false;
}
public U GetValue(T key)
{
U value;
if (TryGetValue(key, out value))
{
return value;
}
throw new KeyNotFoundException(key);
}
因此,如果TryGetValue返回false,则GetValue依赖于TryGetValue并抛出异常。这比从TryGetValue调用GetValue并吞下产生的异常要好得多。
答案 8 :(得分:1)
但是如果TryFoo允许一些异常逃脱,那么它真正说的是“我不会抛出任何我知道如何处理的异常”,然后是合同的全部概念“我不会抛出异常”被扔出窗外。
我想你自己在这里说过:)
TryXXX应该只保证它不会抛出它知道如何处理的异常。如果它不知道如何处理给定的异常,为什么要抓住它呢?同样适合你 - 如果你不知道该怎么做,你为什么要抓住它? BadAllocExceptions例如,(通常)没有太多关于这些,除了在主try / catch块中捕获它们显示一些错误消息并尝试正常关闭应用程序..
答案 9 :(得分:1)
在您的示例中捕获和吞咽所有异常通常是一个坏主意。一些例外情况,尤其是ThreadAbortException
和OutOfMemoryException
,不应该被吞下。实际上,第一个不能被吞下,并且会在你的阻挡块结束时自动被重新抛出,但仍然。
答案 10 :(得分:1)
首先要记住一个例外首先意味着什么:
异常意味着您的方法无法按其名称所做的那样执行。
我们首先实现TryFoo
模式的原因是抛出异常是昂贵的,另外,如果你要求调试器在首次抛出异常时中断,它可以是非常讨厌。因此,如果您的方法具有一种特别常见的故障模式,例如Int32.Parse
,当它被赋予无效输入时抛出ArgumentException
,则会出现问题。
TryFoo
模式背后的想法是,您的方法应该能够指示特别常见的失败模式而不会抛出异常。因此,您应该从不在这些条款中实施它:
bool TryFoo() {
try {
Foo();
return true;
}
catch (SomeKindOfException) {
return false;
}
}
因为这首先颠覆了TryFoo
模式的整个目的。
就抛出异常而言,答案是,是的,有时您的TryFoo
方法需要抛出异常。具体来说:当方法因您期望的特别常见的原因而失败时。此处的示例可能是OutOfMemoryException
,或堆栈溢出,或未部署的预期程序集,或外部Web服务不可用。这些失败模式表明某些事情需要引起注意,不应忽视。
答案 11 :(得分:0)
我曾经参加了一个很棒的演讲,他说他参与了BCL的异常处理策略。不幸的是我忘记了他的名字,我找不到我的笔记。
他描述了这个策略:
这里有趣的一点是#4。我清楚地记得他说明了指南的单一故障模式部分。其他失败仍然会引发异常。
顺便提一下,他还说CLR团队告诉他.Net异常缓慢的原因是因为它们是在SEH之上实现的。他们还表示没有特别需要以这种方式实施(除了权宜之计),如果他们进入真正客户的真正性能问题的前10名,他们会考虑重新实施它们更快!