例外与特殊回报值

编程中哪一个更好?

我不是在谈论完全排他性。 对于以下内容更有用:

list.Find ,你得到default(T)或你的值,而不是ValueNotFoundexception(例子)。

要么

list.IndexOf ,得到-1或正确的索引。

那么,答案取决于它。

如果在列表中找不到项目,则抛出exception是一种可怕的做法,因为该项目可能不在列表中是完全可行的。

现在,如果这是某种类型的专用列表,并且该项应该绝对可以在列表中找到,那么应该抛出exception,因为您遇到了一种不可行/合理的情况。

一般来说,对于诸如业务规则之类的东西,专门的错误代码更好,因为你知道这些事情发生的可能性,并且想要对这些可能性作出反应。 例外情况适用于您不期望的情况,如果发生则无法继续执行代码。

我已经在某个地方读到了一个很好的规则,我非常喜欢。 它说 – “当且仅当它不能执行它所要求的任务时,函数才应该抛出exception”。

所以我通常做的是决定一个函数应该做什么(通常来自业务需求或其他东西),然后为其他一切抛出exception。

如果您已经很好地设计了应用程序,那么您的函数将非常小并且执行具有简单返回值的相对简单的任务。 通过上述规则确定例外情况并不困难。

当然,总是存在模糊的情况(比如字典中没有的密钥)。 那些应该是远在和中间,但在那里你只需要用你的直觉来解决什么是更优雅的解决方案。

哦,所有这一切都永远不会忘记:为了这个工作得很好,只能抓住你可以处理的exception。 大多数情况下,这意味着您只能在上层UI级别捕获它们,您可以在其中显示,向用户显示它们或记录它们。 较低级别可能会在对其自身进行一些处理后使用finally块或重新抛出exception,但是低级别的真正捕获exception通常表示设计不良。

经验法则是仅在发生“不应该发生”的事情时才使用例外。

如果您希望调用IndexOf()可能找不到有问题的值(合理的期望),那么它应该有一个返回代码(如您所说,可能为-1)。 永远不会失败的东西,比如分配内存,应该在失败的情况下抛出exception。

另一件需要记住的事情是,在性能方面处理exception是“昂贵的”。 因此,如果您的代码定期处理exception作为正常操作的一部分,则它将无法以尽可能快的速度执行。

在你提到的情况下,我更喜欢返回值,因为我知道如何处理它。 并且没有理由在同一范围内打扰捕获。

当然,如果你的逻辑构建方式使得值总是应该在列表中,并且它们的缺失是程序员逻辑错误 – exception就是这样。

您可能会喜欢我的两部分博客系列,根据您的编程语言支持的function,在这里讨论了许多权衡,因为它似乎非常相关:

语言function与库设计之间相互作用的一个例子,第一部分

语言特性与图书馆设计之间相互作用的一个例子,第二部分

我要补充一点,我认为这个问题的很多答案都很差(我对我的许多人进行了投票)。 特别糟糕的是API

 if (ItWillSucceed(...)) { DoIt(...) } 

这会产生各种不愉快的问题(详见我的博客)。

你宁愿用哪个?

A:

 item = list.Find(x); 

B:

 If (list.Contains(x)) item = list.Find(x); else item = null; 

C:

 try { item = list.Find(x); } catch { item = null; } 

我愿意打赌答案是A.因此,在这种情况下,返回Default(T)是正确的选择。

更好的语言让您可以满足您的需求。 就像Smalltalk-80一样:

如果id没有用户,以下内容将引发exception:

 user := userDictionary at: id 

这个将评估给定的Block是一个高级函数:

 user := userDictionary at: id ifAbsent: [ "no such id, let's return the user named Anonymous" users detect: [ :each | each name = 'Anonymous' ] ] 

请注意,实际方法是:ifAbsent:。

来自Java背景,在大多数情况下我更喜欢exception。 当一半的代码没有用于检查返回值时,它会使代码更加清晰。

也就是说,它还取决于某些事情可能导致“失败”的频率。 例外情况可能很昂贵,因此您不希望不必要地将它们扔到经常失败的事物上。

Bill Wagner提出的更有效的C#就exception提出了很好的建议。 我们的想法是继续执行操作时抛出exception,只需确保提供挂钩以检查该值是否会引发exception。

例如

 // if this throws an exception list.GetByName("Frank") // throws NotFound exception // provide a method to test list.TryGetByName("Frank") // returns false 

这样你可以通过编写类似的东西来选择退出exception

 MyItem item; if (list.TryGetByName("Frank")) item = list.GetByName("Frank"); 

正如许多与编程有关的问题一样,这一切都取决于

我发现首先应该首先尝试定义您的API,以便首先不会发生exception情况。

使用“按合同设计”可以帮助您完成此任务。 这里会插入引发错误或崩溃的函数并指示编程错误(不是用户错误)。 (在某些情况下,这些检查会在发布模式中删除。)

然后保留一些无法避免的通用故障的例外,例如,DB连接失败,乐观事务失败,磁盘写入失败。

然后通常不需要捕获这些exception,直到它们到达“用户”。 并且将导致用户需要再次尝试。

如果错误是用户错误,如名称中的拼写错误,则直接在应用程序界面代码本身处理。 由于这是一个常见错误,因此需要处理可能已翻译的用户友好错误消息等。

应用程序分层在这里也很有用。 因此,我们举一个从其他帐户转移现金的示例:

 transferInternal( int account_id_1, int account_id_2, double amount ) { // This is an internal function we require the client to provide us with // valid arguments only. (No error handling done here.) REQUIRE( accountExists( account_id_1 ) ); // Design by contract argument checks. REQUIRE( accountExists( account_id_2 ) ); REQUIRE( balance( account_id_1 ) > amount ); ... do the actual transfer work } string transfer( int account_id_1, int account_id_2, double amount ) { DB.start(); // start transaction string msg; if ( !checkAccount( account_id_1, msg ) ) return msg; // common checking code used from multiple operations. if ( !checkAccount( account_id_2, msg ) ) return msg; if ( !checkBalance( account_id_1, amount ) ) return msg; transferInternal( account_id_1, account_id_2, amount ); DB.commit(); // This could fail with an exception if someone else changed the balance while this transaction was active. (Very unlikely but possible) return "cash transfer ok"; } 

从纯粹的设计角度来看,我更喜欢在“exception”事件发生时抛出exception,例如找不到你要求的密钥。 主要原因是它使消费者的生活更轻松 – 他们不必在每次调用您的函数后检查值。 我也喜欢TrySomething函数,因为用户可以显式测试操作是否成功。

不幸的是,.Net中的exception非常昂贵(根据我的经验,通常需要大约50ms才能抛出一个),因此从实际角度来看,您可能希望返回其中一个默认值而不是抛出exception,尤其是在GUI编程中。

我认为它略微具有情境性,但更多地受到返回事件在逻辑上是否exception的支配。 indexOf示例是一个完全正常的输出响应,其中-1是唯一有意义的(或者对于ref类型为null)。

exception应该是:exception,这意味着您需要从真正的exception中获得的所有堆栈跟踪和处理。

对于第一个,我真的不喜欢default(T)选项:如果你有一个int列表,那么0 (可能这是int的默认值;我不使用C#)是一个完全允许的值?

(对Java语法表示道歉,但如果我试图猜测C#,我可能会把它弄得一团糟:))

 class Maybe { public Maybe() { set = false; value = null; } public Maybe(T value) { set = true; this.value = value; } public T get(T defaultVal) { if (set) return value; return defaultVal; } private boolean set; private T value; } 

然后当然Find会返回一个Maybe ,并且调用者会选择一些在此上下文中是合理默认值的值。 当然,当default(T) 正确答案时,你可以添加getDefault作为一种方便的方法。

但是对于IndexOf-1是一个明智的值,因为有效值显然总是> = 0,所以我只是这样做。

在这种情况下我通常做的是定义一个Option类(受F#启发)并使用返回Option类的TryFind()方法扩展IEnumerable。

 public class Option { string _msg = ""; T _item; public bool IsNone { get { return _msg != "" ? true : false; } } public string Msg { get { return _msg; } } internal Option(T item) { this._item = item; this._msg = ""; } internal Option(string msg) { if (String.IsNullOrEmpty(msg)) throw new ArgumentNullException("msg"); this._msg = msg; } internal T Get() { if (this.IsNone) throw new Exception("Cannot call Get on a NONE instance."); return this._item; } public override string ToString() { if (this.IsNone) return String.Format("IsNone : {0}, {1}", this.IsNone, typeof(T).Name); else return String.Format("IsNone : {0}, {1}, {2}", this.IsNone, typeof(T).Name, this._item.ToString()); } 

}

然后你可以用它

 var optionItem = list.TryFind(x => trueorFalseTest() ); if (!optionItem.IsNone) var myItem = optionItem.Get(); 

这样,无论项目是否存在,集合仅遍历一次。

一个例外应该是“特殊”的东西。 因此,如果您调用Find,并且您希望找到某些内容,无论如何,那么如果您没有找到某些内容则抛出exception就是良好的行为。

然而,如果它是正常的流量,你有时候找不到东西,那么抛出exception是不好的。