最佳实践:从属性中抛出异常

Posted

技术标签:

【中文标题】最佳实践:从属性中抛出异常【英文标题】:Best practices: throwing exceptions from properties 【发布时间】:2010-12-02 02:06:55 【问题描述】:

什么时候从属性 getter 或 setter 中抛出异常是合适的?什么时候不合适?为什么?指向有关该主题的外部文档的链接会有所帮助……Google 出现的次数少得惊人。

【问题讨论】:

另见:***.com/questions/633944/… 也相关:***.com/questions/1488322/… 我读了这两个问题,但都没有完全回答这个问题。 必要时。之前的问题和答案都表明允许并赞赏从 getter 或 setter 引发异常,因此您可以简单地“变得聪明”。 【参考方案1】:

我有这段代码,我不确定要抛出哪个异常。

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

【讨论】:

【参考方案2】:

Microsoft 在http://msdn.microsoft.com/en-us/library/ms229006.aspx 上有关于如何设计属性的建议

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

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

您不想在属性 getter 中抛出异常有一些很好的理由:

因为属性“看起来”是字段,所以并不总是很明显它们可以抛出(设计使然)异常;而对于方法,程序员被训练来预期和调查异常是否是调用该方法的预期结果。 许多 .NET 基础架构都使用 Getter,例如序列化程序和数据绑定(例如在 WinForms 和 WPF 中) - 在此类上下文中处理异常可能很快就会成为问题。 当您观察或检查对象时,调试器会自动评估属性获取器。这里的异常可能会令人困惑并减慢您的调试工作。出于同样的原因,在属性中执行其他昂贵的操作(如访问数据库)也是不可取的。 属性通常用于链式约定:obj.PropA.AnotherProp.YetAnother - 使用这种语法,决定在哪里注入异常捕获语句会变得有问题。

作为旁注,人们应该意识到,仅仅因为一个属性没有设计来引发异常,这并不意味着它不会;它可以很容易地调用代码。即使是分配新对象(如字符串)的简单行为也可能导致异常。您应该始终防御性地编写代码,并期望您调用的任何内容都会出现异常。

【讨论】:

如果您遇到像“内存不足”这样的致命异常,无论您是在属性中还是在其他地方获得异常都无关紧要。如果您没有在属性中获得它,那么您只需在几纳秒后从下一个分配内存的事物中获得它。问题不是“属性可以抛出异常吗?”由于致命情况,几乎所有代码都可能引发异常。问题是属性是否应该按设计作为其特定合同的一部分抛出异常。 我不确定我是否理解这个答案中的论点。例如,关于数据绑定 - WinForms 和 WPF 都专门编写为正确处理属性抛出的异常,并将它们视为验证失败 - 这是非常好的(有些人甚至认为它是最好的) 提供域模型验证的方法。 @Pavel - 虽然 WinForms 和 WPF 都可以从属性访问器中的异常中正常恢复,但识别此类错误并从中恢复并不总是那么容易。在某些情况下(例如,当 WPF 中的 Control Template setter 抛出异常时)异常会被默默地吞下。如果您以前从未遇到过此类情况,这可能会导致痛苦的调试会话。 @Steven:那么在特殊情况下,该课程对您有多大用处?如果你不得不编写防御性代码来处理由于一次失败而导致的所有这些异常,并且可能提供合适的默认值,为什么不在你的 catch 中提供这些默认值呢?或者,如果向用户抛出属性异常,为什么不抛出原始的“InvalidArgumentException”或类似的东西,以便他们可以提供缺少的设置文件? 这些是指导方针而不是规则是有原因的;没有指南涵盖所有疯狂的边缘情况。我可能会自己创建这些方法而不是属性,但这是一个判断电话。【参考方案3】:

这是一个非常复杂的问题,答案取决于您的对象的使用方式。根据经验,“后期绑定”的属性 getter 和 setter 不应该抛出异常,而专门“早期绑定”的属性应该在需要时抛出异常。顺便说一句,我认为微软的代码分析工具对属性的使用定义过于狭隘。

“后期绑定”是指通过反射找到属性。例如,Serializeable" 属性用于通过其属性对对象进行序列化/反序列化。在这种情况下抛出异常会以灾难性的方式破坏事物,并且不是使用异常来编写更健壮代码的好方法。

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

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

【讨论】:

【参考方案4】:

一个很好的异常处理方法是使用它们为自己和其他开发人员记录代码,如下所示:

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

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

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

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

此规则的例外(哈哈!)是诸如 IO 之类的异常不受您控制且无法提前检查的情况。

【讨论】:

这个有效且相关的答案是如何被否决的? *** 中应该没有政治,如果这个答案似乎错过了靶心,请为此添加评论。投反对票是针对不相关或错误的答案。【参考方案5】:

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

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

我知道有一个例外,它实际上是一个相当大的例外:任何实现IDisposable 的对象。 Dispose 专门用作使对象进入无效状态的一种方式,甚至还有一个特殊的异常类 ObjectDisposedException 可以在这种情况下使用。在对象被释放后,从任何类成员中抛出 ObjectDisposedException 是完全正常的,包括属性 getter(不包括 Dispose 本身)。

【讨论】:

谢谢帕维尔。这个答案涉及“为什么”,而不是简单地再次说明从属性中抛出异常不是一个好主意。 我不喜欢IDisposable 的所有成员在Dispose 之后都应该变得无用的想法。如果调用成员需要使用 Dispose 已使其不可用的资源(例如,成员将从已关闭的流中读取数据),则成员应抛出 ObjectDisposedException 而不是泄漏,例如ArgumentException,但是如果一个表单的属性表示某些字段中的值,那么允许在处理后读取这些属性(产生最后键入的值)似乎比要求...跨度> ...Dispose 被推迟到所有这些属性都被读取之后。在某些情况下,一个线程可能在一个对象上使用阻塞读取,而另一个线程关闭它,并且数据可能在Dispose 之前的任何时间到达,让Dispose 切断传入数据可能会有所帮助,但允许之前 -接收到要读取的数据。在本来不需要存在的情况下,不应在CloseDispose 之间进行人为的区分。 了解规则的原因可以让您知道何时打破规则(Raymond Chen)。在这种情况下,我们可以看到,如果存在任何类型的不可恢复错误,则不应将其隐藏在 getter 中,因为在这种情况下,应用程序需要尽快退出。 我试图说明的一点是,您的属性获取器通常不应包含允许不可恢复错误的逻辑。如果是这样,则最好使用Get... 方法。这里的一个例外是当您必须实现需要您提供属性的现有接口时。【参考方案6】:

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

在设置器中,如果您的属性应该在类型之外进行验证。例如,名为 PhoneNumber 的属性可能应该具有正则表达式验证,并且如果格式无效应该抛出错误。

对于 getter,可能当值为 null 时,但很可能这是您希望在调用代码上处理的事情(根据设计指南)。

【讨论】:

【参考方案7】:

它几乎不适合 getter,有时适合 setter。

这类问题的最佳资源是 Cwalina 和 Abrams 的“框架设计指南”;它以精装书的形式提供,其中大部分内容也可以在线获得。

来自第 5.2 节:属性设计

避免从 财产获取者。属性吸气剂 应该是简单的操作,应该 没有先决条件。如果是吸气剂 可以抛出异常,它应该 可能被重新设计为一种方法。 请注意,此规则不适用于 索引器,我们确实期望 验证导致的异常 论据。

请注意,本指南仅适用 财产获取者。扔是可以的 属性设置器中的异常。

【讨论】:

虽然(总体上)我同意这样的指导方针,但它认为提供一些额外的见解来说明为什么应该遵循它们是有用的 - 以及当它们被忽略时会产生什么样的后果。跨度> 这与一次性对象有什么关系,以及一旦对象调用了Dispose() 并且随后要求属性值,您应该考虑抛出ObjectDisposedException 的指导?似乎指导应该是“避免从属性获取器中抛出异常,除非对象已被释放,在这种情况下你应该考虑抛出 ObjectDisposedExcpetion”。 设计是在面对相互冲突的需求时找到合理妥协的艺术和科学。无论哪种方式似乎都是合理的妥协。将一个已处置的对象扔到一个属性上,我不会感到惊讶。如果没有,我也不会感到惊讶。由于使用已处置的对象是一种糟糕的编程习惯,因此抱有任何期望是不明智的。 从 getter 中抛出异常是完全有效的另一种情况是,当对象使用类不变量来验证其内部状态时,无论何时进行公共访问都需要检查它它是一种方法或属性【参考方案8】:

MSDN:捕获和抛出标准异常类型

http://msdn.microsoft.com/en-us/library/ms229007.aspx

【讨论】:

以上是关于最佳实践:从属性中抛出异常的主要内容,如果未能解决你的问题,请参考以下文章

在类中抛出异常的最佳方法是什么?

捕获和重新抛出异常的最佳实践是啥?

捕获和重新抛出 .NET 异常的最佳实践

在 finally 块中抛出异常

在锁 c#2 中抛出异常

在Scala中抛出异常,啥是“官方规则”