Answers:
Microsoft在http://msdn.microsoft.com/zh-cn/library/ms229006.aspx上提供了有关如何设计属性的建议。
本质上,他们建议属性获取器是轻量级的访问器,始终可以安全调用。如果您需要抛出异常,他们建议将getter重新设计为方法。对于设置员,他们指出异常是一种适当且可接受的错误处理策略。
对于索引器,Microsoft指出,getter和setter都可以引发异常。实际上,.NET库中的许多索引器都这样做。最常见的例外是ArgumentOutOfRangeException
。
有一些很好的理由说明为什么您不想在属性获取器中引发异常:
obj.PropA.AnotherProp.YetAnother
-使用这种语法,决定在何处注入异常捕获语句变得很成问题。附带说明一下,应该意识到,仅仅因为一个属性并非旨在引发异常,并不意味着它不会。可以很容易地调用这样做的代码。即使分配新对象(如字符串)的简单动作也可能导致异常。您应该始终以防御性的方式编写代码,并期望从您调用的任何内容中引发异常。
从setter抛出异常没有错。毕竟,有什么更好的方法来指示该值对于给定属性无效?
对于吸气剂来说,它通常会被皱眉,这很容易解释:属性吸气剂通常报告对象的当前状态。因此,唯一可行的方法是在状态无效时进行getter抛出。但是,通常也认为设计类是一个好主意,这样一来,就不可能一开始就无法获得无效的对象,或者无法通过常规方式将其置于无效状态(即,始终确保在构造函数中进行完全初始化,并且尝试针对状态有效性和类不变性,使方法异常安全)。只要遵守该规则,您的财产获取者就永远不会陷入必须报告无效状态的情况,从而永远也不会抛出异常。
我知道有一个例外,它实际上是一个相当主要的例外:任何实现的对象IDisposable
。Dispose
专门用于使对象进入无效状态,ObjectDisposedException
在这种情况下甚至可以使用一个特殊的异常类。在处理完对象后ObjectDisposedException
,从任何类成员(包括属性获取器(不包括Dispose
自身))抛出异常是很正常的。
IDisposable
应该在a之后使an的所有成员失效Dispose
。如果调用成员将需要使用Dispose
已变得不可用的资源(例如,该成员将从已关闭的流中读取数据),则该成员应该抛出ObjectDisposedException
而不是泄漏,例如ArgumentException
,但是如果一个成员的形式具有表示某些字段中的值,允许这样的属性在处理后读取(产生最后一个键入的值)比要求更有用……
Dispose
被推迟到所有这些属性都被读取之后。在某些情况下,一个线程可能会在对象关闭时使用阻塞读取,而另一个线程可能会在阻塞之前读取数据Dispose
,这可能会有所帮助Dispose
,但可以截断传入的数据,但允许读取以前接收的数据。一个人不应该强迫之间人为的区分Close
,并Dispose
在没有否则就需要存在的情况。
Get...
方法来代替。当您必须实现要求您提供属性的现有接口时,这里是一个例外。
它几乎永远都不适合使用吸气剂,有时也不适合使用setter。
这类问题的最佳资源是Cwalina和Abrams撰写的“框架设计指南”。它可以作为装订本使用,其中很大一部分也可以在线获得。
从第5.2节:物业设计
避免从属性获取器抛出异常。属性获取器应该是简单的操作,并且不应该具有先决条件。如果getter可以引发异常,则可能应该将其重新设计为一种方法。请注意,此规则不适用于索引器,在索引器中,我们确实期望由于验证参数而导致异常。
请注意,该准则仅适用于属性获取器。可以在属性设置器中引发异常。
ObjectDisposedException
一旦该物品Dispose()
被调用并且随后需要某个属性值的情况下应考虑抛出的指导有何关系?似乎该指南应该是“避免抛出来自属性获取器的异常,除非对象已被处置,在这种情况下,您应该考虑抛出ObjectDisposedExcpetion”。
解决异常的一种好方法是使用它们为您自己和其他开发人员编写代码,如下所示:
例外应针对特殊的程序状态。这意味着可以将它们写在任何位置!
您可能希望将它们放入吸气剂的原因之一是要记录一个类的API-如果软件在程序员尝试使用错误时立即引发异常,那么他们就不会使用错误!例如,如果您在数据读取过程中进行了验证,那么如果数据中存在致命错误,那么继续并访问过程结果可能就没有意义了。在这种情况下,如果有错误,您可能想使输出抛出错误,以确保另一个程序员检查这种情况。
它们是记录子系统/方法/任何事物的假设和边界的一种方式。在一般情况下,不应将它们捕获!这也是因为如果系统按照预期的方式协同工作,则永远不会抛出它们:如果发生异常,则表明未满足一段代码的假设-例如,它没有以这种方式与周围的世界交互它原本打算这样做。如果捕获到为此目的而编写的异常,则可能意味着系统已进入不可预测/不一致的状态-这最终可能导致数据崩溃或损坏或类似情况,这可能更难以检测/调试。
异常消息是报告错误的一种非常粗略的方式-不能通过大批量收集它们,而只能真正包含一个字符串。这使它们不适合报告输入数据中的问题。在正常运行中,系统本身不应进入错误状态。因此,其中的消息应为程序员而不是用户设计-输入数据中的错误内容可以发现并以更合适的(自定义)格式转发给用户。
该规则的Exception(haha!)是类似IO之类的东西,其中的异常不在您的控制范围内,因此无法事先检查。
MSDN:捕获和引发标准异常类型
这是一个非常复杂的问题,答案取决于如何使用您的对象。根据经验,“后期绑定”的属性获取器和设置器不应引发异常,而排他性的“早期绑定”的属性应在需要时引发异常。顺便说一句,在我看来,Microsoft的代码分析工具正在定义属性的使用范围。
“后期结合”是指通过反射发现性质。例如,“可序列化”属性用于通过对象的属性对对象进行序列化/反序列化。在这种情况下引发异常会以灾难性的方式破坏事物,而不是使用异常制作更健壮的代码的一种好方法。
“早期绑定”表示编译器将属性使用绑定在代码中。例如,当您编写的某些代码引用属性获取器时。在这种情况下,可以在有意义的时候抛出异常。
具有内部属性的对象的状态由这些属性的值确定。表示对对象内部状态敏感的敏感属性的属性不应用于后期绑定。例如,假设您有一个必须打开,访问然后关闭的对象。在这种情况下,在不先调用open的情况下访问属性应导致异常。假设在这种情况下,我们不抛出异常,并且允许代码访问值而不抛出异常?即使代码是从无意义的getter中获得的值,也似乎很高兴。现在,我们将调用getter的代码置于糟糕的境地,因为它必须知道如何检查该值以查看它是否是无意义的。这意味着代码必须对从属性getter获得的值进行假设,以便对其进行验证。这就是编写不良代码的方式。
我在不确定该抛出哪个异常的地方有这段代码。
public Person
{
public string Name { get; set; }
public boolean HasPets { get; set; }
}
public void Foo(Person person)
{
if (person.Name == null) {
throw new Exception("Name of person is null.");
// I was unsure of which exception to throw here.
}
Console.WriteLine("Name is: " + person.Name);
}
我通过强制将其作为构造函数中的参数来防止该模型首先使该属性为null。
public Person
{
public Person(string name)
{
if (name == null) {
throw new ArgumentNullException(nameof(name));
}
Name = name;
}
public string Name { get; private set; }
public boolean HasPets { get; set; }
}
public void Foo(Person person)
{
Console.WriteLine("Name is: " + person.Name);
}