将异常捕获为预期的程序执行流控制?
我一直觉得期望定期抛出异常并将其用作流逻辑是一件坏事。异常感觉它们应该是"例外"。如果我们期望并计划一个异常,那似乎表明代码应该被重构,至少在.NET中...
然而。最近的情况让我停顿了一下。我前一段时间在msdn上发布了此消息,但我想对此进行更多讨论,这是一个完美的地方!
因此,假设我们有一个数据库表,该表具有一个其他表的外键(在最初引发辩论的情况下,有4个外键指向该表)。我们希望允许用户删除,但前提是没有外键引用;只有在没有外键引用的情况下,才允许删除用户。我们不想级联删除。
我通常只是检查一下是否有引用,如果有,我会通知用户而不是删除。在LINQ中将相关表作为对象的成员是非常容易和轻松的,因此Section.Projects和Section.Categories等都可以很好地使用intellisense和所有其他功能键入。
但是事实是LINQ然后必须潜在地击打所有4个表,以查看是否有任何结果行指向该记录,而击打数据库显然总是一个相对昂贵的操作。
这个项目的负责人要求我将其更改为仅捕获一个带有547(外键约束)代码的SqlException并以这种方式进行处理。
我曾是...
抵抗的。
但是在这种情况下,吞下与异常相关的开销可能要比吞下4个表命中要有效得多……特别是因为我们必须在每种情况下都进行检查,但是在这种情况下我们可以避免异常没有孩子的时候
再加上数据库确实应该是负责处理参照完整性的人,这就是它的工作,并且做得很好。
所以他们赢了,我改变了。
在某种程度上,我还是觉得不对。
你们对期望和有意处理异常有何看法?看起来比事前检查更有效率吗?这会让下一个开发人员在看代码时更加困惑,还是减少了困惑?因为数据库可能知道开发人员可能不希望添加检查的新外键约束,这是否更安全?还是我们对最佳实践到底是什么持观点看法?
解决方案
领导是绝对正确的。例外不仅在蓝月亮的情况下是一次,而且特别是用于报告预期结果以外的情况。
在这种情况下,仍将进行外键检查,并且可以通过异常机制来通知我们。
我们不应该使用笼统的catchall语句捕获和抑制异常。进行细粒度的异常处理是专门设计异常的原因。
哇,
首先,我们能不能将问题简化一下,而阅读一个经过深思熟虑并能解释的问题真是太好了,需要消化的东西很多。
简短的回答是"是",但这可以视情况而定。
- 我们有一些应用程序,其中许多业务逻辑与SQL查询相关联(不是我的设计Gov!)。如果这是它的结构形式,那么管理人员就很难说服它,因为它"已经起作用"。
- 在这种情况下,真的有意义吗?由于仍然是一次穿越电线并返回的旅程。服务器在意识到它无法继续之前是否做了很多工作(即,如果发生一系列事务来执行操作,那么它是否会浪费一半时间浪费时间?)
- 首先在UI中进行检查是否有意义?它对应用程序有帮助吗?是否提供更好的用户体验? (即,我看到了一些案例,其中我们在向导中逐步执行了几个步骤,当它具有在步骤1之后需要移交的所有信息时,它便会开始然后掉落)。
- 并发问题吗?是否有可能在提交之前删除/编辑记录或者进行其他任何操作(如经典的File.Exists` boo-boo一样)。
在我看来:
我会两者都做。如果我可以快速失败并提供更好的用户体验,那就太好了。无论如何,任何预期的SQL(或者任何其他)异常都应被捕获并适当地反馈。
我知道有一个共识,即除特殊情况外,不应将异常用于其他情况,但是请记住,我们在这里跨越了应用程序界限,什么也没想到。就像我说的,这就像File.Exists
,没有意义,可以在访问之前将其删除。
我们所做的没有错。异常并不是不必要的"异常"。它们的存在是为了允许调用对象根据其需要进行细粒度的错误处理。
我认为我们是对的,异常应该仅用于处理意外结果,这里我们使用异常来处理可能的预期结果,我们应该显式处理这种情况,但仍要捕获异常以显示可能的错误。
除非通过这种方式在整个代码中处理这种情况,否则我会支持我们。仅在实际存在性能问题时才应提出性能问题,即取决于这些表的大小和使用此功能的次数。
捕获特定的SqlException是正确的事情。这是SQL Server通信外键条件的机制。即使我们可能赞成异常机制的其他用法,这也是SQL Server这样做的方式。
同样,在检查四个表期间,某些其他用户可能会在检查完成之前但在读取该表之后添加相关记录。
我建议调用一个存储过程,该过程检查是否存在任何依赖关系,然后删除是否存在任何依赖关系。这样,完整性检查就完成了。
当然,我们可能希望对单例删除和批处理删除使用不同的存储过程...批处理删除可以查找子行,并返回不符合批量删除条件的一组记录(有子行) 。
好问题。但是,我找到了答案……吓人!
例外是一种GOTO。
我不喜欢以这种方式使用异常,因为这会导致意大利面条式代码。
就那么简单。
我不希望在程序中到处都看到异常,但是在这种情况下,我将使用异常机制。
即使我们在LINQ中进行测试,也必须捕获异常,以防在我们使用LINQ测试完整性时有人插入了子记录。由于仍然要检查异常,因此为什么要重复代码?
另一点是,这种"短距离"异常不会导致维护问题,也不会使程序更难以阅读。我们将在10行代码中进行尝试,删除的SQL调用和catch。意图很明显。
不像抛出要捕获的异常那样,堆栈中的5个过程调用处于较高的位置,其问题是要确保中间的所有对象都已被正确处理(释放)。
异常并非总是一个灵丹妙药,但是在这种情况下使用异常不会感到错误。
我的两分钱
伊夫