最佳实践: 从属性引发异常

什么时候适合从属性 getter 或 setter 中引发异常?什么时候不合适?为什么?链接到外部文件的主题将是有帮助的... 谷歌出人意料地几乎没有出现。

57537 次浏览

微软对如何在 http://msdn.microsoft.com/en-us/library/ms229006.aspx上设计属性提出了建议

本质上,他们建议属性 getter 是轻量级的访问器,调用它们总是安全的。如果需要抛出异常,他们建议将 getter 重新设计为方法。对于 setter,它们指出异常是一种适当且可接受的错误处理策略。

对于索引器,Microsoft 表示可以接受 getter 和 setter 抛出异常。事实上,许多索引器在。NET 库做到这一点。最常见的例外是 ArgumentOutOfRangeException

为什么不想在属性 getter 中引发异常,有一些很好的理由:

  • 因为属性“似乎”是字段,所以它们并不总是能够抛出(按设计)异常; 而对于方法,程序员接受的训练是期望并调查异常是否是调用方法的预期结果。
  • 很多人使用 Getter。NET 基础设施,比如序列化器和数据绑定(例如,在 WinForms 和 WPF 中)——在这样的上下文中处理异常会很快成为问题。
  • 当您监视或检查对象时,调试器将自动计算属性 getter。这里的异常可能会使您感到困惑,并减慢您的调试工作。出于同样的原因,在属性中执行其他代价高昂的操作(比如访问数据库)也是不可取的。
  • 属性通常用于链式约定: obj.PropA.AnotherProp.YetAnother-使用这种语法,决定在哪里注入异常 catch 语句就成了问题。

As a side note, one should be aware that just because a property is 不是设计好的 to throw an exception, that doesn't mean it won't; it could easily be calling code that does. Even the simple act of allocating a new object (like a string) could result in exceptions. You should always write your code defensively and expect exceptions from anything you invoke.

它几乎从来不适用于吸气器,有时也适用于塞特器。

对于这类问题,最好的资源是由 Cwalina 和 Abrams 编写的“框架设计指南”; 它可以作为一本装订的书,而且大部分内容也可以在网上找到。

From section 5.2: Property Design

避免从。引发异常 财产获得者,财产获得者 应该是简单的操作和应该 没有先决条件。如果一个 getter 可以抛出异常,它应该 probably be redesigned to be a method. 请注意,此规则不适用于 索引器,我们期望的地方 由于验证而导致的异常 the arguments.

请注意,本指南仅适用于 属性获取器。可以抛出 属性设置器中的异常。

这些都在 MSDN 中有记录(在其他答案中有链接) ,但是这里有一个一般的经验法则..。

在 setter 中,如果应验证属性的类型高于或超过。例如,名为 PhoneNumber 的属性可能应该具有正则表达式验证,如果格式无效,则应该引发错误。

For getters, possibly when the value is null, but most likely that is something you will want to handle on the calling code (per the design guidelines).

从 setter 引发异常没有错。毕竟,还有什么更好的方法来表明该值对给定的属性无效呢?

对于 getter,它通常是不被接受的,这可以很容易地解释: 一般来说,属性 getter 报告对象的当前状态; 因此,只有当状态无效时,getter 才有理由抛出。但是通常认为设计你的类是一个好主意,这样就不可能最初得到一个无效的对象,或者通过正常的方式把它放入无效的状态(例如,始终确保构造函数中的完全初始化,并尝试使方法对于状态有效性和类不变量是异常安全的)。只要您坚持该规则,您的属性 getter 就不应该进入必须报告无效状态的情况,从而永远不会抛出。

There is one exception I know of, and it's actually a rather major one: any object implementing IDisposable. Dispose is specifically intended as a way to bring object into an invalid state, and there's even a special exception class, ObjectDisposedException, to be used in that case. It is perfectly normal to throw ObjectDisposedException from any class member, including property getters (and excluding Dispose itself), after the object has been disposed.

处理 Exception 的一个好方法是使用它们为您自己和其他开发人员编写代码文档,如下所示:

异常应该是针对异常程序状态的。这意味着可以在任何想要的地方编写它们!

One reason you might want to put them in getters is to document the API of a class - if the software throws an exception as soon as a programmer tries to use it wrong then they wont use it wrong! For instance if you have validation during a data reading process it may not make sense to be able to continue and access the results of the process if there were fatal errors in the data. In this case you may want to make getting the output throw if there were errors to ensure that another programmer checks for this condition.

They are a way of documenting the assumptions and boundaries of a subsystem/method/whatever. In the general case they should not be caught! This is also because they are never thrown if the system is working together in the way expected: If an exception happens it shows that the assumptions of a piece of code are not met - eg it is not interacting with the world around it in the way it was originally intended to. If you catch an exception that was written for this purpose it probably means the system has entered an unpredictable/inconsistent state - this may ultimately lead to a crash or corruption of data or similar which is likely to be much harder to detect/debug.

异常消息是报告错误的一种非常粗糙的方式——它们不能集中收集,只能真正包含一个字符串。这使得它们不适合在输入数据中报告问题。在正常运行时,系统本身不应该进入错误状态。因此,它们中的消息应该是为程序员而不是用户设计的——输入数据中的错误可以被发现,并以更合适的(自定义)格式转发给用户。

例外(哈哈!)对于这个规则,类似 IO 的情况是,异常不在您的控制之下,无法事先检查。

这是一个非常复杂的问题,答案取决于如何使用对象。根据经验,“后期绑定”的属性 getter 和 setter 不应该引发异常,而只有“早期绑定”的属性应该在需要时引发异常。顺便说一句,在我看来,微软的代码分析工具对属性的定义过于狭隘。

“延迟绑定”意味着通过反射找到属性。例如,Serializable“属性用于通过对象的属性序列化/反序列化对象。在这种情况下抛出异常会以灾难性的方式破坏事情,这不是使用异常创建更健壮的代码的好方法。

“早期绑定”意味着编译器将属性使用绑定到代码中。例如,当您编写的某些代码引用属性 getter 时。在这种情况下,可以在异常有意义时抛出异常。

具有内部属性的对象具有由这些属性的值确定的状态。表示对对象内部状态敏感的属性的属性不应用于后期绑定。例如,假设您有一个必须打开、访问然后关闭的对象。在这种情况下,不先调用 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);
}

我首先通过在构造函数中强制将其作为参数来防止模型的属性为空。

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);
}