例外与特殊回报值

时间:2009-03-23 17:14:56

标签: c# .net exception

编程中哪一个更好?

我不是在谈论完全排他性。它更适用于:

list<T>.Find,您获得default(T)或您的值,而不是ValueNotFound例外(示例)。

list<T>.IndexOf,你得到-1或正确的索引。

15 个答案:

答案 0 :(得分:16)

嗯,答案取决于它。

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

现在,如果这是某种特殊列表,并且项目应该在列表中找到绝对,那么应该抛出一个异常,因为你遇到了一个不可行的情况/合理的。

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

答案 1 :(得分:11)

我已经阅读了一个关于此的一个很好的规则,我非常喜欢。它说 - “当且仅当它不能执行它所要执行的任务时,函数才应该抛出异常。”

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

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

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

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

答案 2 :(得分:6)

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

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

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

答案 3 :(得分:3)

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

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

答案 4 :(得分:3)

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

An example of the interplay between language features and library design, part one

An example of the interplay between language features and library design, part two

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

中的API特别糟糕
if (ItWillSucceed(...)) {
    DoIt(...)
}

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

答案 5 :(得分:3)

您更愿意使用哪种?

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)是正确的选择。

答案 6 :(得分:2)

更好的语言可以让您做任何符合您需求的事情。就像在Smalltalk-80中一样:

如果没有id的用户,则以下内容会引发异常:

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:。

答案 7 :(得分:1)

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

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

答案 8 :(得分:1)

Bill Pagner提出的更有效的C#在例外情况方面提出了很好的建议。我们的想法是继续执行操作时抛出异常,只需确保提供挂钩以检查该值是否会引发异常。

例如

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

通过编写类似

的方式,您可以选择退出异常
MyItem item;
if (list.TryGetByName("Frank"))
  item = list.GetByName("Frank");

答案 9 :(得分:1)

正如许多与编程有关的问题都取决于......

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

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

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

通常不需要捕获这些例外,直到它们到达“用户”。并且会导致用户需要重试。

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

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

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";
}

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

首先,我不太喜欢default(T)选项:如果int列表中有0(可能是int的默认值,该怎么办? ;我不使用C#)是一个完全允许的值?

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

class Maybe<T> {
    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<T>,并且调用者在此上下文中选择一个合理默认值的值。当然,当getDefault 正确答案时,您可以添加default(T)作为便捷方法。

对于IndexOf-1是一个合理的值,因为有效值显然总是&gt; = 0,所以我只是这样做。

答案 13 :(得分:0)

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

public class Option<T>
{
    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();

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

答案 14 :(得分:-1)

异常应该是“特殊”的东西。因此,如果你打电话给Find,并且你希望找到一些东西,无论如何,那么如果你没有找到某些东西则抛出异常就是好行为。

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