抛出正确类型的例外

时间:2011-01-05 15:12:28

标签: c# .net exception

在我的代码中,我经常遇到这样的情况:

public void MyMethod(string data)
{
    AnotherClass objectOfAnotherClass = GetObject(data);
    if (objectOfAnotherClass == null)
        throw new WhatExceptionType1("objectOfAnotherClass is null.");

    if (objectOfAnotherClass.SomeProperty < 0)
        throw new WhatExceptionType2("SomeProperty must not be negative.");
}

想象一下,GetObject使用了一些不受我控制的外部库,如果null没有对象,则该库返回data并认为是否为SomeProperty作为有效状态,因此不会抛出异常。进一步想象MyMethod在没有objectOfAnotherClass的情况下无法工作,而对于负SomeProperty则没有意义。

WhatExceptionType1/2抛出这种情况的正常例外是什么?

基本上我有四个选择:

  • 1)InvalidOperationException,因为MyMethod在上述条件下没有意义。另一方面,指南(以及VS中的Intellisense)表示如果方法所属的对象处于无效状态,则应抛出InvalidOperationException。现在对象本身不处于无效状态。相反,输入参数data和基于此参数的其他一些操作会导致MyMethod无法再运行的情况。

  • 2)ArgumentException,因为方法可以使用data的值,方法不能使用其他值。但我不能通过单独检查data来检查这一点,我必须在决定之前调用其他操作。

  • 3)Exception,因为我不知道要使用哪种其他异常类型,因为所有其他预定义的异常都过于专业化而且不适合我的情况。

  • 4)MyCustomException(我自己的异常类型派生自Exception)。这似乎总是一个选项,但我担心当我开始遵循这种模式时,我必须为许多不同的错误条件定义许多特殊的异常类。

还有其他更好的选择吗?支持或反对这些选择的理由是什么?

提前感谢您的反馈!

9 个答案:

答案 0 :(得分:8)

如果存在有意义的内置异常,我会使用它们。如果没有,那么滚动自己的异常是有意义的 - 即使它是一个扩展Exception的空类 - 因为这允许您检测特定的异常类型。例如,如果您只是抛出异常,您如何知道异常是因为objectOfAnotherClass为空,并且GetObject中没有引发异常?

总而言之:特定异常更好,因为您可以(可能)诊断并从特定情况中恢复。因此,使用内置的.NET异常(如果它们足够),或者滚动你自己的例外。

编辑:我应该澄清一点,我很少使用现有的例外情况并在其中添加消息。如果异常类型告诉您错误,而不是必须调试,生成异常,然后检查消息以查看问题是什么,它会使您的代码更具可读性。

答案 1 :(得分:5)

使用内置异常类型时要小心...它们对.NET框架有非常特殊的含义,除非你使用它来完全> 相同的含义,最好滚动你的拥有(Serializeable的约翰·桑德斯+1)。

InvalidOperationException具有以下含义:

  

当方法调用对象的当前状态无效时引发的异常。

例如,如果您拨打SqlConnection.Open(),则在未指定数据源的情况下会获得InvalidOperationExceptionInvalidOperationException不适合您的方案。

ArgumentException也不合适。创建objectOfAnotherClass失败可能与传入的错误数据无关。假设它是文件名,但GetObject()没有读取文件的权限。在编写方法时,无法知道为什么对GetObject()的调用失败,并且您可以说最好的是返回的对象为空或无效。

Exception一般来说只是一个坏主意......它让调用者完全不知道为什么该方法无法创建对象。 (就此而言,只有catch (Exception ex) {..}也是一个坏主意)

您需要明确识别出错的异常,这意味着创建自己的异常。尽量保持通用,以避免1,000个自定义异常。我建议:

ObjectCreateException:   // The call to GetObject() returned null<br />
InvalidObjectException:  // The object returned by GetObject() is invalid 
                         // (because the property < 0)

感谢投票〜我想我会添加一些我们写过的自定义异常的例子。

请注意,您实际上不需要向方法添加任何代码,因为自定义异常实际上并没有做出与其基类不同的任何操作。他们只代表不同的东西。第二个例子确实为异常添加了一个属性,因此有代码支持它(包括构造函数)。

第一个是我们所有例外的通用基础;规则“不要捕获一般异常”适用(尽管我们仍然这样做......它允许我们区分我们生成的异常和框架抛出的异常)。第二个是更具体的异常,它派生自Gs3Exception并序列化自定义属性。

.NET开发团队认为ApplicationException没有真正的价值并且不赞成它,但我的纯粹主义者总是喜欢它,所以它仍然存在于我的代码中。但是,在这里,它确实没有增加任何价值,只增加了继承层次的深度。所以可以直接从Exception继承。

/// <summary>
/// A general, base error for GS3 applications </summary>
[Serializable]
public class Gs3Exception : ApplicationException {


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class </summary>
    public Gs3Exception() {}


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class </summary>
    /// <param name="message">A brief, descriptive message about the error </param>
    public Gs3Exception(string message) : base(message) {}


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class 
    /// when deserializing </summary>
    /// <param name="info">The object that holds the serialized object data </param>
    /// <param name="context">The contextual information about the source or
    ///  destination.</param>
    public Gs3Exception(SerializationInfo info, StreamingContext context) : base(info, context) { }


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class
    /// with a message and inner exception </summary>
    /// <param name="Message">A brief, descriptive message about the error </param>
    /// <param name="exc">The exception that triggered the failure </param>
    public Gs3Exception(string Message, Exception exc) : base(Message, exc) { }


}


/// <summary>
/// An object queried in an request was not found </summary>
[Serializable]
public class ObjectNotFoundException : Gs3Application {

    private string objectName = string.Empty;

    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    public ObjectNotFoundException() {}


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    /// <param name="message">A brief, descriptive message about the error</param>
    public ObjectNotFoundException(string message) : base(message) {}


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    /// <param name="ObjectName">Name of the object not found </param>
    /// <param name="message">A brief, descriptive message about the error </param>
    public ObjectNotFoundException(string ObjectName, string message) : this(message) {
        this.ObjectName = ObjectName;
    }


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class.
    /// This method is used during deserialization to retrieve properties from 
    /// the serialized data. </summary>
    /// <param name="info">The object that holds the serialized object data.</param>
    /// <param name="context">The contextual information about the source or
    /// destination.</param>
    public ObjectNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) {
        if (null != info) {
            this.objectName = info.GetString("objectName");
        }
    }


    /// <summary>
    /// When serializing, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> 
    /// with information about the exception. </summary>
    /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds 
    /// the serialized object data about the exception being thrown.</param>
    /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
    /// <exception cref="T:System.ArgumentNullException">
    /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic) </exception>
    /// <PermissionSet>
    ///     <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/>
    ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/>
    /// </PermissionSet>
    [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags=SecurityPermissionFlag.SerializationFormatter)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context) {

        base.GetObjectData(info, context);

        //  'info' guaranteed to be non-null (base.GetObjectData() will throw an ArugmentNullException if it is)
        info.AddValue("objectName", this.objectName);

    }


    /// <summary>
    /// Gets or sets the name of the object not found </summary>
    /// <value>The name of the object </value>
    public string ObjectName {
        get { return objectName; }
        set { objectName = value; }
    }

}

PS:在有人给我打电话之前,基础Gs3Exception不会增加ApplicationException的价值的原因是企业库异常处理应用程序块......通过申请 - 基本级别异常,我们可以为我们的代码直接抛出的异常创建一般日志记录策略。

答案 2 :(得分:4)

如果不是两者,我的投票至少会是ArgumentException。应该抛出ArgumentExceptions,引用“当提供给方法的其中一个参数无效时”。如果MyMethod无法使用参数data创建{My}的有效实例,正如MyMethod所期望的那样,该参数在MyMethod中无效。

了解除非您计划捕获不同类型的异常并以不同方式处理它们,否则确切地抛出哪个异常并不重要。一些异常(如ArgumentNullException)基于非常少的信息创建自定义消息,因此易于设置和本地化,其他(如SqlExceptions)具有更多关于出错的具体数据,但如果您只想要的话,所有这些都是多余的抛出一个说“哎呀!”的例外。

答案 3 :(得分:4)

在这里做的恰当的事情是如果可以提供帮助就抛出异常。正确的事情中缺少两条基本的信息。首先是字符串参数,显然这个方法的调用者对GetObject()如何工作知道适当的字符串应该有一些秘密知识。因此,您需要将调用者视为权限,他比您更了解GetObject()。

其次是GetObject()行为。显然,作者设计它是为了非异常并期望它返回null。一个更强大的契约是两个方法,TryGetObject()和GetObject(),后者抛出。或者更常见的是GetObject()的额外参数名为 throwOnFailure 。但是没有发生这种情况,返回null并且正常。

你在这违反了合同。你试图把它从非特殊变为异常,但没有任何线索有什么不对。最好的办法是不要更改合同,并将其留给方法的调用方处理。毕竟,那是知道GetObject()做什么的那个。更改方法的名称,使用“尝试”一词,然后返回一个布尔值。

这一切都假设GetObject()的作者知道他在做什么。如果他没有,你几乎无法改善这种情况。如果您有理由认为调用者可能已经搞砸了,则抛出ArgumentException,如果您认为GetObject()作者可能在其代码中有错误,则抛出NullReferenceException。

答案 4 :(得分:3)

如何使用ObjectNotFoundException。这将正确描述情况。

答案 5 :(得分:3)

首先,对错误进行分类。 Eric Lippert on his blog拥有我见过的最好的分类:致命,愚蠢,烦恼和外生。你的例外并不是致命的;它将是:愚蠢的,烦恼的或外生的。

错误是 boneheaded ,如果您可以说正确的输入name,您知道GetObject将返回一个对您的方法有意义的对象。换句话说,这些异常的唯一原因是调用MyMethod的代码中的错误。在这种情况下,您使用的异常类型并不重要,因为您永远不应该在生产中看到它 - ArgumentException(如果问题出在name)或InvalidOperationException(如果问题在于定义MyMethod的对象的状态在这种情况下是很好的选择,但是不应该记录特定的异常类型(否则它将成为API的一部分)。

如果GetObject相对于name是可预测的,则错误 vexing (即,对于给定的name,您将始终获得有效对象或永远不会得到有效的对象),但name基于用户输入(或配置)。在这种情况下,您根本不想抛出异常;你想编写一个TryMyMethod方法,以便调用代码不必捕获异常来处理非异常情况。

如果GetObject的{​​{1}}行为无法预测{<1}},则错误为外生:某些外部影响可能导致name有效或无效。在这种情况下,您必须选择特定的异常类型并将其记录为作为API的一部分。 name不是外生异常的好选择; ArgumentException也不会。您应该选择更像InvalidOperationException的内容,更能描述潜在的问题(无论FileNotFoundException做什么)。

答案 6 :(得分:2)

我知道这个答案远远不能用于OP,但我倾向于认为这里的难点更多的是代码味道。如果我们在方法层次上考虑单一责任原则而不仅仅是一个类,那么看起来这种方法违反了它。它做了两件事:它解析data然后进行进一步处理。所以正确的做法是消除一个责任,即解析。您可以通过将参数更改为AnotherClass的实例来完成此操作。然后你会明白你应该做什么:

public void MyMethod(AnotherClass data)
{
    if (data == null)
        throw new ArgumentNullException("data is null.");

    if (data.SomeProperty < 0)
        throw new ArgumentException("data.SomeProperty must not be negative.");

    ...
}

然后调用解析方法并处理这种情况成为调用者的责任。他们可能会在调用方法之前选择捕获异常或预先检查它。呼叫者必须做更多的工作,但具有增加灵活性的好处。例如,这也提供了允许AnotherClass的替代构造的好处;调用者可以在代码中构建它,而不是解析它或调用一些替代的解析方法。

答案 7 :(得分:1)

这实际上取决于您计划如何处理应用程序中的异常。在try / catch情况下自定义异常很好但是try / catches也很昂贵。如果您不打算捕获和处理自定义异常,则:抛出新的异常(“索引超出范围:SomeProperty不得为负数。”);和自定义异常一样有用。

public class InvalidStateException : ApplicationException
{
   ...
}

在您的代码中:

// test for null
if(objectOfAnotherClass == null) throw new NullReferenceException("Object cannot be null");

// test for valid state
if(objectOfAnotherClass.SomeProperty < 0) throw new InvalidStateException("Object is in an invalid state");

答案 8 :(得分:1)

我同意Stephen Cleary的帖子,你应该尝试对你的异常进行分类,然后找到相应的异常类型。我发现Eric Lippert的分类非常有趣,并且在许多方面他是对的,但是,我一直在考虑另一种类型的分类,看起来有点像Lippert的分类,除了我更专注于代码合同。我打算做的是在失败的前置条件后置条件和纯错误中对Exceptions进行分类。

事情是:每个方法都有一个正式的契约,如果你满足所有先决条件,它承诺满足一些后置条件。通常,所有前提条件可以细分为三种类型:

  • 与安全相关(是否有权拨打电话?)
  • 与上下文相关(处于正确状态的对象是否进行调用?)
  • 与输入相关(调用者是否指定了有效参数?)

原则上,每个方法都应该按顺序检查这些条件并抛出以下异常类型之一(确切的类型或派生类型):

  • SecurityException
  • InvalidOperationException
  • ArgumentException

这些异常与调用者通信是“他们的错”出了问题并且调用无法正确完成。但是,如果根据方法的正式规范满足所有前提条件,并且方法检测到它不能满足指定的后置条件,则它应该抛出一个Exception类型,清楚地传达什么出错。您通常希望为这些情况定义自己的Exception类型,或者重用现有的Exception类型,只要它不是为前置条件失败保留的类型之一。

最后,CLR引发的错误是Exceptions,几乎可以在任何时间,任何时间发生,几乎无法预测。它们永远不会明确地成为您的方法的一部分,因此不应该被用户代码抛出(既不是专门处理的)。从这个意义上说,他们几乎一对一地映射到Lippert的致命例外。

那么,对于Slauma提出的案例,我应该怎么做?

嗯,正如您可以想象的那样,它完全取决于MyMethod(data)GetObject(data)objectOfAnotherClass.SomeProperty的合同。 GetObject(data)的API在哪些情况下会返回null(或抛出它自己的异常)? SomeProperty的确切含义和规格是什么?

假设GetObject(data)是一个返回从数据库检索的对象并且data是对象标识符的方法。如果GetObject(data)的合同指定如果不存在标识符为null的对象,则它将返回data,那么您的设计应该在其自己的合同中考虑到这一点。您可能希望强制调用者(如果合理)始终指定data的值,使GetObject(data)不返回null,如果确实如此,则可以抛出{{1} (或衍生)表示来电方的错误。

另一方面,如果ArgumentException指定当调用者没有足够的权限来检索对象时它只返回null,则可以抛出GetObject(data)(或派生词)来指示问题。调用者SecurityException

最后,如果MyMethod(data)承诺它“永远不会”返回GetObject(data),它仍然存在,您可能会允许您的代码与null崩溃(因为正义假设它永远不会当你处理敏感代码时,要特别处理这种情况,抛出你自己的异常类型(因为后置条件,从MyMethod的角度来看,失败了)。

第二种情况稍微复杂一些,但可以采用相同的方式。假设NullReferenceException提供了由标识符null从数据库中提取的行或实体,并且objectOfAnotherClass明确指出data应指定对象的标识符MyMethod(data) 1}},然后在这种情况下抛出ArgumentException(或衍生物)将成为您设计的一部分。

然后再次,当data在一个上下文中操作时,调用者可能会认为有效的标识符永远不会返回一个objectOfAnotherClass.SomeProperty >= 0的对象,(或者更好:甚至不知道存在这样的对象)然后这个事件在调用者的网站上是真正意外的,MyMethod(data)应该不要显式地检查案例(再次,因为假设它不会发生),或者,如果要更强大的代码,抛出一个特定的自定义异常表明问题。

总结:我认为确定要抛出的objectOfAnotherClass.SomeProperty < 0的类型取决于每个方法与其呼叫者和被叫者的正式合同。如果调用者不满足前提条件,则方法应始终通过抛出MyMethod(data)ExceptionSecurityException来响应调用者。如果方法本身无法满足它自己的后置条件,或者可能会崩溃CLR或其他组件抛出的异常,或者抛出它自己更具体的Exception来指示问题。