在适当的情况下抛出异常是构造函数工作的一部分。
让我们考虑为什么我们需要构造函数。
其中一个部分是方便拥有设置各种属性的方法,并且可能会执行一些更高级的初始化工作(例如,FileStream
实际上将访问相关文件)。但是,如果始终如此方便,我们就不会有时候发现成员初始化器很方便了。
构造函数的主要原因是使我们能够维护对象不变量。
对象不变量是我们可以在每个方法调用的开始和结束时对对象说的话。(如果它被设计用于并发使用,我们甚至会有在方法调用期间保持的不变量)。
Uri
类的不变量之一是,如果 IsAbsoluteUri
为 true,则 Host
将是一个有效的主机字符串(如果 IsAbsoluteUri
为 false,则 Host
可能是一个有效的主机,如果它是方案相关的,或者访问它可能会导致一个 InvalidOperationException
)。
因此,当我正在使用这样一个类的对象并已检查了 IsAbsoluteUri
后,我知道我可以访问 Host
而不会出现异常。我也知道它确实是一个主机名,而不是例如有关贝珍石在中世纪和近代早期信仰中的避邪作用的简短论文。
好吧,将这样的论文放入代码中并不太可能,但是将某些垃圾放入对象中的代码肯定是存在的。
维护不变量归结为确保对象持有的值组合始终合理。这必须在任何更改对象的属性设置器或方法(或通过使对象不可变,因为如果从未进行更改,则永远不会有无效更改)以及最初设置值的方法(即构造函数中)中完成。
在强类型语言中,我们从类型安全性中获得了一些检查(必须在0
和 15
之间的数字永远不会被设置为"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()
时,可能会产生副作用。重要的是,只有在完成了任何可能引发异常的情况之后才尝试执行它。
同样地,在实践中这很容易做到。将所有验证检查放在前面是很自然的,大多数情况下这就是你需要做的所有事情。但是,一个重要的情况是如果您正在构造过程中获得未管理的资源。如果在这样的构造函数中引发异常,则意味着该对象未被构建。因此它不会被最终化或处置,并且因此未受到释放未管理的资源。
有关避免这种情况的几个指南:
首先不直接使用未管理的资源。如果可能的话,请通过包装它们的托管类来处理,以便那个对象负责。
如果确实必须使用未管理的资源,请不要执行任何其他操作。
如果您需要具有未管理的资源和其他状态的类,则结合上述两个指南;创建一个仅处理未管理的资源的包装类,并在您的类中使用它。
- 最好使用
SafeHandle
尽可能持有指向未管理的资源的指针。这很好地解决了第二点的许多问题。
那么,关于捕获异常呢?
我们当然可以这样做。问题是,在捕获到某些东西时应该怎么做?请记住,我们必须创建与我们要求的对象相匹配的对象,或引发异常。大多数情况下,如果我们在其中一些尝试中失败,则没有任何方法可以成功构建对象。因此,我们很可能只是让异常通过,或者捕获异常仅抛出从调用构造函数方面更合适的不同异常。
但是如果我们可以在catch
之后继续有意义地进行下去,那么是允许的。
所以总的来说,“你可以在构造函数中使用throw或try和catch吗?”的答案是:“可以”。
但是有一个问题。如上所述,在构造函数中抛出异常的好处在于任何new
要么得到一个有效的对象,要么抛出异常;没有中间状态,要么有该对象,要么没有。
然而,静态构造函数是整个类的构造函数。如果实例构造函数失败,你不会得到一个对象,但如果静态构造函数失败,你将不会得到一个类!
在应用程序的余生(严格来说,在应用程序域的余生)内,你几乎注定无法再尝试利用该类或从该类派生的任何类。因此,在静态类中抛出异常通常是一个非常糟糕的主意。如果有可能某些尝试失败的事情会在另一个时间尝试成功,则不应在静态构造函数中这样做。
唯一需要在静态构造函数中抛出异常的情况是当你希望应用程序完全失败时。例如,在缺少重要配置设置的Web应用程序中抛出异常很有用;当然,每个请求都失败并显示相同的错误消息是很烦人的,但这意味着你一定会解决该问题!