构造函数中的异常处理

Yve*_*omb 5 c# constructor exception-handling

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

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

public Chat()
{
    chatClient = new Client(Configuration.LoginEmail, Configuration.LoginPassword);
    chatRoom = chatClient.JoinRoom(Configuration.RoomUrl);
}
Run Code Online (Sandbox Code Playgroud)

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

Jon*_*nna 22

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

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

一部分是方便的设置各种属性的方法,并且可能做一些更高级的初始化工作(例如,FileStream实际上将访问相关文件).但是如果它真的那么方便我们有时候会发现会员初始化者不方便.

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

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

Uri该类的一个不变量是if如果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");
Run Code Online (Sandbox Code Playgroud)

所以如果你打电话new List<string>(-2)或者new List<int>(null)你得到一个例外,而不是一个无效的列表.

关于这种情况需要注意的一件有趣的事情是,这种情况下,他们可能会为调用者提供"固定"的东西.在这种情况下,可以安全地将负数视为0与空数相同,并将null枚举视为与空数相同.无论如何他们决定扔.为什么?

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

  1. 来电者为我们提供了可以直接使用的价值观.

呃,用它们.

  1. 来电者给了我们根本无法有意义使用的价值观.(例如,将枚举值设置为未定义值).

绝对抛出异常.

  1. 来电者为我们提供了可以按摩有用价值的价值观.(例如,将一些结果限制为负数,我们可以将其视为与零相同).

这是一个棘手的案例.我们需要考虑:

  1. 意义是否明确无误?如果有多种方法可以考虑它"真正"意味着什么,那么抛出异常.

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

如有疑问,请抛出异常.首先,如果你对自己应该做什么有疑问,那么调用者很可能对他们应该期待你做什么产生怀疑.对于另一个,最好稍后返回并将代码转换为代码,而不是将代码转换为不会抛出代码的代码,因为后者更有可能将工作用途转化为损坏的应用程序.

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

new FileStream(@"D:\logFile.log", FileMode.Open);
Run Code Online (Sandbox Code Playgroud)

在这次通话中没有任何无效应该绝对失败.所有验证检查都应该通过.它有望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);
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们有两种情况都可能发生抛出.首先,我们已经验证了伪造的论点.之后我们调用了FileStream构造函数,而构造函数又可能抛出异常.

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

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

在本回答的开头部分考虑了大多数验证案例,我们做事的顺序并不重要.使用方法或属性,我们必须确保我们已将事物更改为处于有效状态或抛出异常并使事情单独处理,否则即使异常是,我们仍然可以使对象处于无效状态抛出(在大多数情况下,它足以在您更改任何内容之前执行所有验证).对于构造函数,事情的顺序并不重要,因为在这种情况下我们不会返回一个对象,所以如果我们抛出,我们就没有把任何垃圾放到应用程序中.

通过上述调用new FileStream(),可能会产生副作用.重要的是,只有在抛出异常的任何其他情况完成后才会尝试它.

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

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

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

  2. 如果必须使用非托管资源,请不要执行任何其他操作.

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

  1. 更好的是,SafeHandle如果可能的话,用来保存指向非托管资源的指针.这很好地处理了第2点的大量工作.

现在.捕获异常怎么样?

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

但当然,如果我们能够在catch允许之后继续有意义地继续下去.

总而言之,答案是"你能在构造函数中使用throw还是尝试捕获?" 是是的".

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

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

在将来的任何尝试中,你几乎都注定要在应用程序的剩余生命周期中使用该类或任何派生的类(严格来说,应用程序域的其余部分).在大多数情况下,这意味着在静态类中抛出异常是一个非常糟糕的主意.如果可能尝试尝试某些事情并且失败可能会再次尝试,那么它不应该在静态构造函数中完成.

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

  • 我可能已经更简洁地介绍了相同的内容,但它是在家务和孩子争吵之间零碎地写的,所以Pascal对写一封长信的道歉,因为他没有时间写一个短信. (3认同)