c# - 构造函数中的异常处理

标签 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);行可以引发异常。

最佳答案

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

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

一方面是拥有一种设置各种属性的方法的便利,也许还有一些更高级的初始化工作(例如FileStream实际上将访问相关文件)。但是,如果真的那样一直很方便,我们有时会发现成员初始化器不方便。

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

对象不变性是我们可以在每个方法调用的开始和结束时都说到的对象。 (如果它是为并发使用而设计的,我们甚至在方法调用期间将拥有不变式)。

Uri类的不变式之一是,如果IsAbsoluteUri为true,则Host将是有效主机的字符串,(如果IsAbsoluteUri为false,则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),则会得到一个异常,而不是一个无效的列表。

关于这种情况要注意的一件有趣的事情是,在这种情况下,他们可能已经为呼叫者“固定”了东西。在这种情况下,将负数视为与0相同,将空可枚举数视为与空值相同将是安全的。他们决定还是扔。为什么?

好吧,在编写构造函数(以及其他方法)时,我们要考虑三种情况。


来电者提供了我们可以直接使用的价值。


嗯,用它们。


呼叫者为我们提供了我们根本无法有意义使用的价值观。 (例如,将枚举中的值设置为未定义的值)。


绝对会引发异常。


来电者给我们提供了价值,我们可以将其转化为有用的价值。 (例如,将结果数限制为负数,我们可以将其视为零)。


这是比较棘手的情况。我们需要考虑:


含义是明确的吗?如果有多种方法可以考虑“真正”的含义,则抛出异常。
呼叫者是否可能以合理的方式得出了这个结果?如果该值只是简单的愚蠢,则调用者可能在将其传递给构造函数(或方法)时犯了一个错误,并且您在隐藏它们的错误方面没有任何帮助。一方面,他们很可能在其他通话中犯其他错误,但这种情况变得显而易见。


如有疑问,请抛出异常。一方面,如果您不确定应该怎么做,则呼叫者可能会对他们应该做什么表示怀疑。对于另一个,最好是稍后再返回,将抛出的代码转换为不运行的代码,而不是将未抛出的代码转换为可以运行的代码,因为后者将更有可能将工作用途转换为残破的应用程序。

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

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%的时间内完成的全部工作。但是,重要的情况是,如果您在构造函数的过程中获得了非托管资源。如果在此类构造函数中引发异常,则意味着该对象未被构造。这样,它就不会完成或处置,因此,不会释放非托管资源。

避免这种情况的一些准则:


首先不要直接使用非托管资源。如果有可能,请遍历将它们包装起来的托管类,这就是该对象的问题。
如果您必须使用非托管资源,则无需执行其他任何操作。


如果您需要同时具有非托管资源和其他状态的类,请结合上面的两个准则;创建一个仅处理非托管资源的包装器类,并在您的类中使用它。


更好的是,尽可能使用SafeHandle保留指向非托管资源的指针。这很好地处理了第2点的许多工作。


现在。捕获异常呢?

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

但是可以肯定的是,如果我们可以有意义地在catch之后继续操作,那是允许的。

因此,总的来说,答案是“您可以在构造函数中使用抛出或尝试捕获吗?”是是的”。

美中不足的是一只苍蝇。如上所示,在构造函数中进行抛出的妙处在于,任何new要么获取有效对象,要么抛出异常。两者之间没有,您要么拥有那个对象,要么就没有。

但是,静态构造函数是整个类的构造函数。如果实例构造函数失败,则不会得到对象,但是如果静态构造函数失败,则不会得到类!

在应用程序的整个剩余生命中(严格来说,应用程序的整个剩余生命),在将来尝试使用该类或任何派生自该类的尝试时,您几乎注定要失败。在大多数情况下,这意味着在静态类中引发异常是一个非常糟糕的主意。如果有可能尝试失败的尝试又一次又成功了,那么不应在静态构造函数中完成。

大约只有一次您希望应用程序完全失败时,您才需要抛出静态构造函数。例如,丢入缺少重要配置设置的Web应用程序很有用;当然,让每个请求都失败并显示相同的错误消息很令人讨厌,但这意味着您一定要解决该问题!

关于c# - 构造函数中的异常处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35373776/

相关文章:

c# - 我可以阻止 .net 4.0 对单引号进行编码吗?

c# - 在 C# 中获取两个 'DateTime' 之间的所有 DateTimes

c# - SqlDataReader 仅返回 SqlCommand 中的第一个选择

java - 在构造函数中从另一个类设置值

java - 调用构造函数但没有输出

c# - 如何使用 linq to entities 将数据绑定(bind)到 asp.net treeview?

c++ - 在构造函数中将 std::string 转换为 QString

C# - 重新抛出异常而不将其设置为变量

java - Spring @ControllerAdvice 不起作用

c# - try/catch/throw 和 try/catch(e)/throw e 的区别