构造函数中的异常处理

时间:2016-02-12 23:10:33

标签: c# constructor exception-handling

您可以在构造函数中使用throwtry-catch吗? 如果是这样,拥有一个可以引发异常的参数的构造函数的目的是什么?

这个构造函数是一个例子:

public Chat()
{
    chatClient = new Client(Configuration.LoginEmail, Configuration.LoginPassword);
    chatRoom = chatClient.JoinRoom(Configuration.RoomUrl);
}

chatRoom = chatClient.JoinRoom(Configuration.RoomUrl);可以抛出异常。

3 个答案:

答案 0 :(得分:18)

在适当的时候抛出异常是构造函数的一部分。

让我们考虑为什么我们有构造函数。

一部分是方便设置各种属性的方法,也许还有一些更高级的初始化工作(例如FileStream实际上会访问相关文件)。但是如果真的那么方便我们总是不方便找到会员初始化者。

构造函数的主要原因是我们可以维护对象不变量。

对象不变量是我们可以在每个方法调用的开头和结尾处对对象说的。 (如果它是为并发使用而设计的,我们甚至会在方法调用期间保留不变量。)

Uri类的一个不变量是,如果IsAbsoluteUri为真,那么Host将是一个有效主机的字符串,(如果IsAbsoluteUri为假Host可能是有效的主机,如果它是方案相对的,或者访问它可能会导致InvalidOperationException。)

因此,当我使用此类课程的对象并且我已经检查IsAbsoluteUri时,我知道我可以无异常地访问Host。我也知道它确实是一个主机名,而不是例如关于中世纪和早期现代信仰对牛黄的特性的简短论述。

好吧,所以有些代码把这样的论文放在那里是不可能的,但代码将某些类的垃圾放入对象当然是。

保持不变量归结为确保对象所持有的值组合始终有意义。这必须在任何改变对象的属性设置器或方法中完成(或者通过使对象不可变,因为如果你从未进行过更改,就永远不会有无效的更改)以及最初设置值的那些,也就是说在构造函数中。

在强类型语言中,我们从该类型安全性中获得一些检查(必须在015之间的数字永远不会设置为"Modern analysis has found that bezoars do indeed neutralise arsenic.",因为编译器只是不让你这么做。)但其余的呢?

考虑带有参数的List<T>的构造函数。其中一个采用整数,并相应地设置内部容量,另一个采用列表填充的IEnumerable<T>。这两个构造函数的开头是:

public List(int capacity)
{
    if (capacity < 0) throw new ArgumentOutOfRangeException("capacity", capacity, SR.ArgumentOutOfRange_NeedNonNegNum);

/* … */

public List(IEnumerable<T> collection)
{
    if (collection == null)
        throw new ArgumentNullException("collection");

因此,如果您拨打new List<string>(-2)new List<int>(null),则会收到异常,而不是无效的列表。

关于这种情况需要注意的一件有趣的事情是,这种情况下他们可能已经修复了#34;呼叫者的事情。在这种情况下,可以安全地将负数视为与0相同,并将null枚举视为与空数相同。无论如何他们决定扔。为什么呢?

好吧,我们在编写构造函数时会考虑三种情况(实际上还有其他方法)。

  1. 来电者为我们提供了可以直接使用的价值。
  2. 呃,使用它们。

    1. 来电者为我们提供了我们无法有意义地使用的价值观。 (例如,将枚举值设置为未定义的值)。
    2. 绝对抛出异常。

      1. 来电者为我们提供了可以按摩到有用价值的价值观。 (例如,将一些结果限制为负数,我们可以将其视为与零相同)。
      2. 这是一个棘手的案例。我们需要考虑:

        1. 意思是否明确?如果不止一种方式来考虑它真的&#34;真的&#34;意味着,然后抛出异常。

        2. 来电者是否可能以合理的方式达到此结果?如果值只是简单的愚蠢,那么调用者可能会错误地将它传递给构造函数(或方法),并且你不会在隐藏错误时对它们做任何好处。首先,他们很可能在其他电话中犯了其他错误,但这种情况很明显。

        3. 如果有疑问,请抛出异常。一方面,如果您对自己应该做什么表示怀疑,那么调用者可能会对他们应该做什么表示怀疑。对于另一个人来说,最好稍后回来并将代码转换为代码,而不是将代码转换为不会抛出代码的代码,因为后者更有可能转向工作用于破碎的应用程序。

          到目前为止,我只查看了可以视为验证的代码;我们被要求做些傻事,我们拒绝了。另一种情况是,当我们被要求做一些合理的事情(或愚蠢,但我们无法检测到这一点)时,我们无法做到这一点。考虑:

          new FileStream(@"D:\logFile.log", FileMode.Open);
          

          此次通话中没有任何内容无效,绝对应该失败。所有验证检查都应该通过。它有望在阅读模式下以D:\logFile.log打开文件,并为我们提供一个FileStream对象,我们可以通过该对象访问它。

          但如果没有D:\logFile.log怎么办?或者没有D:\(相同的东西,但内部代码可能以不同的方式失败)或我们没有权限打开它。或者它被另一个进程锁定了?

          在所有这些情况下,我们都没有做出要求。我们返回一个表示尝试读取全部失败的文件的对象并不好!所以,我们再次抛出异常。

          好。现在考虑StreamReader()采用路径的情况。它的工作方式有点像(例如为了示例而调整为切出一些间接):

          public StreamReader(String path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
          {
              if (path==null || encoding==null)
                  throw new ArgumentNullException((path==null ? "path" : "encoding"));
              if (path.Length==0)
                  throw new ArgumentException(Environment.GetResourceString("Argument_EmptyPath"));
              if (bufferSize <= 0)
                  throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
              Contract.EndContractBlock();
          
              Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan, Path.GetFileName(path), false, false, true);
              Init(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, false);
          }
          

          我们在这两种情况下都可能发生投掷。首先,我们已经对伪造的论点进行了验证。之后,我们调用FileStream构造函数,这反过来可能会抛出异常。

          在这种情况下,只允许异常通过。

          现在我们需要考虑的案例有点复杂。

          在本回答的开头部分考虑了大多数验证案例,我们做事的顺序并不重要。使用方法或属性,我们必须确保我们要么将事物更改为有效状态,要么抛出异常并将事情单独留下,否则我们仍然可以将对象置于无效状态,即使抛出异常(大多数情况下,在更改任何内容之前完成所有验证就足够了)。对于构造函数来说,事情的顺序并不重要,因为在这种情况下我们不会返回一个对象,所以如果我们扔掉它,我们就不会把任何垃圾扔进去申请。

          虽然上面调用了new FileStream(),但可能会产生副作用。重要的是,只有在任何其他会引发异常的案例完成后才会尝试。

          在大多数情况下,这在实践中很容易实现。将所有验证检查放在第一位是很自然的,而这一切只需99%的时间。但重要的一个案例是,如果您在构造函数的过程中获取非托管资源。如果在这样的构造函数中抛出异常,则意味着该对象未被构造。因此,它不会被最终确定或处理,因此非管理资源不会被释放。

          关于避免这种情况的一些指导原则:

          1. 首先不要直接使用非托管资源。如果可能的话,通过托管它们的托管类进行工作,这就是该对象的问题。

          2. 如果您必须使用非托管资源,请不要做任何其他事情。

          3. 如果您需要一个同时具有非托管资源和其他状态的类,请将上述两条指南结合起来;创建一个只处理非托管资源的包装类,并在类中使用它。

            1. 更好的是,如果可能的话,使用SafeHandle来保存指向非托管资源的指针。这很好地处理了第2点的大量工作。
            2. 现在。如何捕获异常?

              我们当然可以这样做。问题是,当我们抓到什么东西时,我们该怎么办?请记住,我们必须创建一个与我们要求匹配的对象,或者抛出异常。大多数情况下,如果我们在其中尝试过的事情之一失败了,那么我们无法做任何事情来成功构建对象。因此,我们可能只是让异常通过,或者从调用构造函数的人的角度来捕获异常只是为了抛出一个更合适的异常。

              但当然,如果我们能够在catch之后继续有意义地继续,那么这是允许的。

              总而言之,答案是&#34;你可以在构造函数中使用throw或try并捕获吗?&#34;是,&#34;是&#34;。

              美国药膏中只有一只苍蝇。如上所述,在构造函数中抛出的好处是任何new都可以获得有效对象,否则抛出异常;中间没有,你要么拥有那个对象,要么就没有。

              静态构造函数虽然是整个类的构造函数。如果一个实例构造函数失败,你就不会得到一个对象,但如果一个静态构造函数失败,你就不会上课!

              在将来尝试使用该类或任何衍生的类时,您将注定应用程序的其余部分(严格来说,应用程序的其余部分)域)。在大多数情况下,这意味着在静态类中抛出异常是一个非常糟糕的主意。如果可能尝试尝试某些事情并尝试失败并在另一时间成功,则不应该在静态构造函数中完成。

              关于您想要引入静态构造函数的唯一时间是您希望应用程序完全失败的时候。例如,投入一个缺乏重要配置设置的Web应用程序是有用的;确定,让每一个请求都失败并显示相同的错误消息很烦人,但这意味着您确定要解决该问题!

答案 1 :(得分:2)

  

你可以在构造函数中使用throw或try并捕获吗?

两者都有可能。

如果在构造对象实例期间可能发生异常,并且您可以对此进行操作,请抓住它并尝试修复它。

如果您无法对异常做任何事情,通常最好允许它传播(而不是让对象实例处于不正确的初始化状态)。

答案 2 :(得分:1)

如果在实例构造函数中抛出异常,则会中断对象的正常生命周期。因此,如果对象具有析构函数来清理任何资源,那么构造函数中抛出的异常将阻止析构函数运行,从而形成内存泄漏。如果您在构造函数或任何字段中分配了任何可用资源,请始终捕获,清理和重新抛出。

如果在静态构造函数中抛出异常,则该类不再处于AppDomain可以使用的状态。所以总是在静态构造函数中捕获异常并处理它们。 重新投掷并让他们不被捕获。

在任何构造函数中捕获和处理异常都可以。

但是按照正常情况,编码以避免异常而不是让它们出现会好1000倍。

MSDN Constructor Design&amp; Eric Lippert's Vexing Exceptions