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


110

什么时候从属性获取器或设置器中引发异常?什么时候不合适?为什么?链接到有关该主题的外部文档将很有帮助... Google出现的结果很少。




1
我读了这两个问题,但IMO都没有完全回答这个问题。
乔恩·塞格尔

必要时。前面的问题和答案都表明,允许并赞赏从getter或setter引发异常非常重要,因此您可以简单地“变得聪明”。
Lex Li

Answers:


134

Microsoft在http://msdn.microsoft.com/zh-cn/library/ms229006.aspx上提供了有关如何设计属性的建议。

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

对于索引器,Microsoft指出,getter和setter都可以引发异常。实际上,.NET库中的许多索引器都这样做。最常见的例外是ArgumentOutOfRangeException

有一些很好的理由说明为什么您不想在属性获取器中引发异常:

  • 因为属性“似乎”是字段,所以并不总是很明显它们会引发(按设计)异常。而使用方法,则需要训练程序员预期和调查异常是否是调用该方法的预期结果。
  • Getter被许多.NET基础结构使用,例如序列化程序和数据绑定(例如,在WinForms和WPF中)-在这种情况下处理异常可能会迅速成为问题。
  • 当您观察或检查对象时,调试器会自动评估属性获取器。此处的异常可能会造成混乱,并减慢调试工作的速度。出于相同的原因,也不希望在属性中执行其他昂贵的操作(例如访问数据库)。
  • 属性通常在链接约定中使用:obj.PropA.AnotherProp.YetAnother-使用这种语法,决定在何处注入异常捕获语句变得很成问题。

附带说明一下,应该意识到,仅仅因为一个属性并非旨在引发异常,并不意味着它不会。可以很容易地调用这样做的代码。即使分配新对象(如字符串)的简单动作也可能导致异常。您应该始终以防御性的方式编写代码,并期望从您调用的任何内容中引发异常。


41
如果遇到致命异常,例如“内存不足”,则无论是在属性中还是在其他地方获取异常都无关紧要。如果您没有在属性中获取它,那么您将在下一个分配内存的事物之后几纳秒的时间内获取它。问题不是“财产会引发异常吗?” 由于致命的情况,几乎所有代码都会引发异常。问题是,财产在设计上是否应将异常作为其特定合同的一部分。
Eric Lippert

1
我不确定我是否理解此答案中的论点。对于xample,关于数据绑定- 专门编写WinForms和WPF 来正确处理属性引发的异常,并将它们视为验证失败-这是提供域模型验证的一种非常好的方法(甚至有人认为这是最好的) 。
帕维尔·米纳夫09年

6
@Pavel-尽管WinForms和WPF都可以从属性访问器中的异常中正常恢复,但是识别并从此类错误中恢复并不总是那么容易。在某些情况下(例如,在WPF中,控制模板设置器引发异常),该异常会被静默吞下。如果您以前从未遇到过这种情况,这可能会导致痛苦的调试会话。
LBushkin,2009年

1
@Steven:那么在特殊情况下该课程对您有多少用处?如果随后由于一个故障而不得不编写防御性代码来处理所有这些异常,并且大概提供了适当的默认值,为什么不在捕获中提供这些默认值呢?或者,如果将属性异常抛出给用户,为什么不只抛出原始的“ InvalidArgumentException”或类似的内容,以便它们可以提供缺少的设置文件呢?
Zhaph-Ben Duguid

6
这些是准则而不是规则是有原因的;没有指南涵盖所有疯狂的情况。我可能会使用这些方法,而不是自己使用属性,但这是一个判断。
埃里克·利珀特

34

从setter抛出异常没有错。毕竟,有什么更好的方法来指示该值对于给定属性无效?

对于吸气剂来说,它通常会被皱眉,这很容易解释:属性吸气剂通常报告对象的当前状态。因此,唯一可行的方法是在状态无效时进行getter抛出。但是,通常也认为设计类是一个好主意,这样一来,就不可能一开始就无法获得无效的对象,或者无法通过常规方式将其置于无效状态(即,始终确保在构造函数中进行完全初始化,并且尝试针对状态有效性和类不变性,使方法异常安全)。只要遵守该规则,您的财产获取者就永远不会陷入必须报告无效状态的情况,从而永远也不会抛出异常。

我知道有一个例外,它实际上是一个相当主要的例外:任何实现的对象IDisposableDispose专门用于使对象进入无效状态,ObjectDisposedException在这种情况下甚至可以使用一个特殊的异常类。在处理完对象后ObjectDisposedException,从任何类成员(包括属性获取器(不包括Dispose自身))抛出异常是很正常的。


4
谢谢帕维尔。这个答案是“为什么”,而不是简单地再次声明从属性中抛出异常不是一个好主意。
SolutionYogi

1
我不赞成这样的观念,即绝对IDisposable应该在a之后使an的所有成员失效Dispose。如果调用成员将需要使用Dispose已变得不可用的资源(例如,该成员将从已关闭的流中读取数据),则该成员应该抛出ObjectDisposedException而不是泄漏,例如ArgumentException,但是如果一个成员的形式具有表示某些字段中的值,允许这样的属性在处理后读取(产生最后一个键入的值)比要求更有用……
supercat 2012年

1
... Dispose被推迟到所有这些属性都被读取之后。在某些情况下,一个线程可能会在对象关闭时使用阻塞读取,而另一个线程可能会在阻塞之前读取数据Dispose,这可能会有所帮助Dispose,但可以截断传入的数据,但允许读取以前接收的数据。一个人不应该强迫之间人为的区分Close,并Dispose在没有否则就需要存在的情况。
supercat 2012年

了解规则的原因可以让您知道何时违反规则(Raymond Chen)。在这种情况下,我们可以看到,如果存在任何类型的不可恢复的错误,则不应将其隐藏在getter中,因为在这种情况下,应用程序需要尽快退出。
2015年

我要说明的一点是,您的属性获取器通常不应包含会导致无法恢复的错误的逻辑。如果确实如此,则最好将其作为一种Get...方法来代替。当您必须实现要求您提供属性的现有接口时,这里是一个例外。
帕维尔·米纳夫2015年

24

它几乎永远都不适合使用吸气剂,有时也不适合使用setter。

这类问题的最佳资源是Cwalina和Abrams撰写的“框架设计指南”。它可以作为装订本使用,其中很大一部分也可以在线获得。

从第5.2节:物业设计

避免从属性获取器抛出异常。属性获取器应该是简单的操作,并且不应该具有先决条件。如果getter可以引发异常,则可能应该将其重新设计为一种方法。请注意,此规则不适用于索引器,在索引器中,我们确实期望由于验证参数而导致异常。

请注意,该准则仅适用于属性获取器。可以在属性设置器中引发异常。


2
尽管(我总体上)同意这样的准则,但认为有必要提供一些其他信息以了解为什么应遵循这些准则,以及忽略这些准则会产生什么样的后果,这很有用。
LBushkin

3
这与一次性物品以及ObjectDisposedException一旦该物品Dispose()被调用并且随后需要某个属性值的情况下应考虑抛出的指导有何关系?似乎该指南应该是“避免抛出来自属性获取器的异常,除非对象已被处置,在这种情况下,您应该考虑抛出ObjectDisposedExcpetion”。
Scott Dorman

4
设计是面对冲突的需求找到合理折衷方案的艺术和科学。两种方式似乎都是一种合理的折衷;如果将已处置的对象扔到属性上,我不会感到惊讶。如果没有的话,我也不会感到惊讶。由于使用已处置的对象是一种糟糕的编程习惯,因此抱有任何期望都是不明智的。
Eric Lippert

1
从getter内引发异常完全有效的另一种情况是,当对象使用类不变式来验证其内部状态时,无论何时使用方法或属性,都需要在进行公共访问时进行检查
陷阱

2

解决异常的一种好方法是使用它们为您自己和其他开发人员编写代码,如下所示:

例外应针对特殊的程序状态。这意味着可以将它们写在任何位置!

您可能希望将它们放入吸气剂的原因之一是要记录一个类的API-如果软件在程序员尝试使用错误时立即引发异常,那么他们就不会使用错误!例如,如果您在数据读取过程中进行了验证,那么如果数据中存在致命错误,那么继续并访问过程结果可能就没有意义了。在这种情况下,如果有错误,您可能想使输出抛出错误,以确保另一个程序员检查这种情况。

它们是记录子系统/方法/任何事物的假设和边界的一种方式。在一般情况下,不应将它们捕获!这也是因为如果系统按照预期的方式协同工作,则永远不会抛出它们:如果发生异常,则表明未满足一段代码的假设-例如,它没有以这种方式与周围的世界交互它原本打算这样做。如果捕获到为此目的而编写的异常,则可能意味着系统已进入不可预测/不一致的状态-这最终可能导致数据崩溃或损坏或类似情况,这可能更难以检测/调试。

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

该规则的Exception(haha!)是类似IO之类的东西,其中的异常不在您的控制范围内,因此无法事先检查。


2
这个有效且相关的答案是如何被否决的?StackOverflow中应该没有任何政治可言,如果这个答案似乎错过了靶心,请添加注释以表示这种效果。否定投票是针对不相关或错误的答案。
辩论者2015年

1

这全部记录在MSDN中(与其他答案链接),但这是一般的经验法则...

在设置器中,是否应该在类型之外验证您的财产。例如,一个名为PhoneNumber的属性可能应该具有正则表达式验证,并且如果格式无效,则应该引发错误。

对于getter,可能是当该值为null时,但是最有可能的是您将要对调用代码进行处理(根据设计准则)。



0

这是一个非常复杂的问题,答案取决于如何使用您的对象。根据经验,“后期绑定”的属性获取器和设置器不应引发异常,而排他性的“早期绑定”的属性应在需要时引发异常。顺便说一句,在我看来,Microsoft的代码分析工具正在定义属性的使用范围。

“后期结合”是指通过反射发现性质。例如,“可序列化”属性用于通过对象的属性对对象进行序列化/反序列化。在这种情况下引发异常会以灾难性的方式破坏事物,而不是使用异常制作更健壮的代码的一种好方法。

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

具有内部属性的对象的状态由这些属性的值确定。表示对对象内部状态敏感的敏感属性的属性不应用于后期绑定。例如,假设您有一个必须打开,访问然后关闭的对象。在这种情况下,在不先调用open的情况下访问属性应导致异常。假设在这种情况下,我们不抛出异常,并且允许代码访问值而不抛出异常?即使代码是从无意义的getter中获得的值,也似乎很高兴。现在,我们将调用getter的代码置于糟糕的境地,因为它必须知道如何检查该值以查看它是否是无意义的。这意味着代码必须对从属性getter获得的值进行假设,以便对其进行验证。这就是编写不良代码的方式。


0

我在不确定该抛出哪个异常的地方有这段代码。

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);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.