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

129

何时从属性getter或setter中抛出异常合适?何时不合适?为什么?在此主题上的外部文档链接将会很有帮助...谷歌搜索结果惊人地少。


参见:https://dev59.com/q3RB5IYBdhLWcg3wZmhz - Jon B
还有相关的内容:https://dev59.com/Y3I_5IYBdhLWcg3wMf9_#1488346 - Brian Rasmussen
1
我阅读了这两个问题,但在我看来,都没有完全回答这个问题。 - Jon Seigel
以前的问题和答案都表明,从 getter 或 setter 函数中引发异常是允许且可以接受的,所以你可以简单地“聪明一些”。 - Lex Li
8个回答

151

微软有关于如何设计属性的建议,可以在http://msdn.microsoft.com/en-us/library/ms229006.aspx找到。

实际上,他们建议属性getter应该是轻量级的访问器,并且始终安全可用。如果抛出异常是必要的话,则建议重新设计getter为方法。对于setter来说,他们表示异常是一种适当和可接受的错误处理策略。

对于索引器,微软表示getters和setters都可以抛出异常,事实上,.NET库中的许多索引器都是这样做的。最常见的异常是ArgumentOutOfRangeException

以下是不希望在属性getter中抛出异常的一些很好的理由:

  • 因为属性“看起来”像字段,所以并不总是明显它们可以抛出(经过设计的)异常;而对于方法,程序员已经被训练去期待并调查是否调用该方法会导致异常。
  • 许多.NET基础结构(如序列化器和数据绑定)使用getter - 在这些上下文中处理异常可能会迅速变得麻烦。
  • 属性getter在您查看或检查对象时由调试器自动评估。这里的异常可能会使您困惑并减慢您的调试工作。出于同样的原因,在属性中执行其他昂贵的操作(例如访问数据库)也是不可取的。
  • 属性经常用于链接约定:obj.PropA.AnotherProp.YetAnother - 对于这种语法,决定在何处注入异常捕获语句变得困难。

顺带一提,应该知道,仅因为属性“未设计”抛出异常,并不意味着它不会抛出异常;它很容易调用会导致异常的代码。即使是分配新对象(如字符串)的简单操作也可能导致异常。您应该始终以防御性编写代码,并从任何您调用的地方预期异常。


46
如果你遇到像“内存不足”这样的致命异常,无论是在属性中还是其他地方,都几乎没有任何区别。如果你没有在属性中遇到它,那么你只会在稍后分配内存的下一个操作中遇到它。问题不在于“属性能否引发异常?”几乎所有代码都可以由于致命条件而引发异常。问题在于一个属性是否应该在其特定合同的设计中作为引发异常的一部分。 - Eric Lippert
1
我不确定我理解这个回答中的论点。例如,关于数据绑定 - WinForms 和 WPF 都是 特别 写成的,以正确处理由属性引发的异常,并将它们视为验证失败 - 这是一种完全正常的(甚至有人认为是最好的)提供领域模型验证的方式。 - Pavel Minaev
6
@Pavel - 虽然 WinForms 和 WPF 都能从属性访问器的异常中优雅地恢复,但有时很难识别和恢复此类错误。在某些情况下(例如,在 WPF 中,当控件模板设置器抛出异常时),异常会被默默地吞噬。如果您以前从未遇到过这种情况,这可能导致痛苦的调试会话。 - LBushkin
6
这些只是指南而非规则,原因在于没有一份指南能够涵盖所有疯狂的特例情况。如果是我自己的话,也许会将它们制作成方法而非属性,但这需要判断力。 - Eric Lippert
1
这些是指南,像大多数指南一样,有时打破它们比强制执行它们更好。例如,当未分配任何值时,.NET Nullable<T>.Value 的getter会抛出异常。可以使用GetValue()方法来完成,但是对我来说,使用属性似乎更好。 - Aidiakapi
显示剩余11条评论

40

从setter中抛出异常没有问题。毕竟,有什么更好的方法来指示给定属性的值无效呢?

对于getter来说,通常不鼓励这样做,并且可以很容易地解释:通常情况下,属性getter报告对象的当前状态。因此,在getter抛出异常的情况下,唯一合理的情况是状态无效。但是,通常认为设计您的类的时候应该使其不可能通过正常方式获得无效对象,或者将其置于无效状态(即始终在构造函数中确保完全初始化,并尝试使方法与状态有效性和类不变式相关联)。只要遵守这个规则,您的属性getter就不会进入必须报告无效状态的情况,因此不会抛出异常。

我知道一个例外,这实际上是一个相当重要的例外:任何实现IDisposable的对象。 Dispose专门用作将对象置于无效状态的方法,甚至有一个特殊的异常类ObjectDisposedException用于在这种情况下使用。在对象被处理后,从任何类成员,包括属性getter(但不包括Dispose本身)抛出ObjectDisposedException是很正常的。


5
谢谢 Pavel。这个回答讲解了“为什么”不建议从属性中抛出异常,而不是简单地重复说明它不是一个好的做法。 - SolutionYogi
1
我不喜欢绝对所有 IDisposable 成员在 Dispose 后都变得无用的概念。如果调用成员需要使用 Dispose 已经不可用的资源(例如,成员将从已关闭的流中读取数据),则该成员应抛出 ObjectDisposedException 而不是泄漏例如 ArgumentException,但如果有一个表单,其中的属性表示某些字段中的值,那么允许在处理后读取这些属性(产生最后键入的值)似乎比要求... - supercat
1
在读取所有这些属性之后,建议将Dispose推迟到之后。在某些情况下,其中一个线程可能会在另一个线程关闭它时对对象进行阻塞读取,并且数据可能在Dispose之前的任何时间到达,此时有助于让Dispose截断传入的数据,但允许先前接收到的数据被读取。在不需要存在的情况下,不应在CloseDispose之间强制进行人为区分。 - supercat
了解规则的原因可以让你知道何时打破规则(Raymond Chen)。在这种情况下,我们可以看到,如果出现任何类型的不可恢复错误,就不应该将其隐藏在getter中,因为在这种情况下,应用程序需要尽快退出。 - Ben
我想要表达的是,你的属性获取器通常不应包含可能导致无法恢复的错误的逻辑。如果有这种情况,最好将其作为 Get... 方法而不是属性获取器。一个例外是当你必须实现需要你提供属性的现有接口时。 - Pavel Minaev
一个 getter 抛出异常的经典例子是数据库类中链接对象的延迟加载;如果由于某种原因加载失败,应该抛出异常,因为否则你无法区分失败和链接对象为空之间的差别。 - Nyerguds

25

对于 getter,几乎从不适用,而对于 setter,有时适用。

这类问题的最佳资源是 Cwalina 和 Abrams 的《框架设计指南》,它可以作为一本纸质书籍购买,也有大量在线可用的内容。

来自第 5.2 节:属性设计

避免从属性 getter 中抛出异常。属性 getter 应该是简单操作,并且不应该有前提条件。如果 getter 可能会抛出异常,那么它可能需要重新设计为一个方法。请注意,此规则不适用于索引器,在其中我们预期由于验证参数而引发异常。

请注意,此指南仅适用于属性 getter。在属性 setter 中抛出异常是可以的。


2
虽然我一般同意这些指南,但我认为提供一些额外的洞察力以解释为什么应该遵循它们,以及当忽视它们时可能会引起什么样的后果是有用的。 - LBushkin
3
这与一次性对象有关,根据指导方针,在调用Dispose()后,如果有请求属性值的操作,应该考虑抛出ObjectDisposedException。因此,指导方针似乎应该是“尽量避免从属性 getter 中抛出异常,除非该对象已被处理,此时应该考虑抛出 ObjectDisposedException”。 - Scott Dorman
4
设计是在面对冲突需求时找到合理妥协的艺术和科学。无论哪种方式都似乎是一个合理的妥协;如果一个已释放对象抛出一个属性,我不会感到惊讶;如果它没有这样做,我也不会感到惊讶。由于使用已释放对象是糟糕的编程实践,因此有任何期望都是不明智的。 - Eric Lippert
1
另一个情况下,从getter中抛出异常是完全有效的,即当对象利用类不变量来验证其内部状态时,需要在进行公共访问时进行检查,无论是方法还是属性。 - Trap

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

我通过在构造函数中强制将属性作为参数来防止模型在一开始就为空。

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

1

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

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

对于getter,在值为空时可能需要处理,但最有可能是您希望在调用代码中处理它(根据设计指南)。


1

关于异常的一个好方法是将其用于为自己和其他开发人员记录代码,具体如下:

异常应该用于表示异常程序状态。这意味着你可以在任何地方编写它们!

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

它们是记录子系统/方法/任何内容的假设和边界的一种方式。通常情况下,它们不应该被捕获!这也是因为如果系统按预期方式协同工作,它们永远不会被抛出:如果发生异常,它表明某段代码的假设未得到满足 - 例如,它与周围的世界交互的方式与最初预期的方式不同。如果你捕获了为此目的编写的异常,那么很可能意味着系统已经进入了不可预测/不一致的状态 - 这最终可能导致崩溃或数据损坏或类似的问题,这很可能更难以检测/调试。

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

这个规则的例外是像IO这样的东西,其中异常不受您的控制,也无法提前检查。


2
这个有效且相关的回答为什么会被踩?在StackOverflow上不应该有政治问题,如果这个回答似乎没有击中要害,请添加评论以表明这一点。踩是针对那些不相关或错误的回答的。 - debater

0

0
这是一个非常复杂的问题,答案取决于对象的使用方式。 一般来说,“后期绑定”的属性getter和setter不应该抛出异常,而仅具有“早期绑定”的属性在需要时应该抛出异常。 顺便说一下,我认为Microsoft的代码分析工具过于狭隘地定义了属性的使用。
“后期绑定”意味着通过反射找到属性。 例如,“Serializable”属性通过其属性序列化/反序列化对象。 在这种情况下,抛出异常会以灾难性的方式破坏事情,并不是使用异常使代码更加健壮的好方法。
“早期绑定”意味着编译器将属性用法绑定在代码中。 例如,当您编写的某些代码引用属性getter时。 在这种情况下,可以在有意义时抛出异常。
一个具有内部属性的对象具有由这些属性的值确定的状态。表达了对对象内部状态敏感和知晓的属性不应用于后期绑定。例如,假设您有一个必须打开、访问、然后关闭的对象。在这种情况下,在没有先调用open的情况下访问属性应该会导致异常。假设在这种情况下我们没有抛出异常,并且允许代码访问一个非常规getter的值?代码看起来很高兴,即使它从一个非常规getter中获取了一个值。现在我们把调用getter的代码放在了一个糟糕的境地,因为它必须知道如何检查这个值是否是非常规的。这意味着代码必须做出关于从属性getter获取的值的假设,以便验证它。这就是糟糕的代码编写方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接