何时抛出异常?

3
尽管我阅读了很多关于异常处理的内容,但我仍然不确定何时应该抛出异常,何时不应该。
例如,我有一个三层结构的API,在DB层中可能会发生事件。
  • 尝试从数据库接收客户端,但找不到给定id的客户。
  • 尝试通过id删除客户,但在数据库中找不到该id。
  • 尝试通过id更新客户,但在数据库中找不到该id。
第一种情况下我不会抛出异常,因为没有真正的“错误发生”。我的存储库函数只返回“null”告诉上层找不到任何内容。
但是其他两种情况已经棘手了。
如果未找到ID,则通过deleteById函数返回“null”毫无意义。如果删除失败,则可以返回“false”,如果删除成功,则返回“true”。但是,这样我必须将其从数据库层传输到领域层,再传递到表示层。对我来说,抛出异常只是简单的解决方法。
但是,在这种情况下,我也没有产生“意外行为”。就像第一种情况一样,没有什么“出错”的事情。难道没有什么“最佳实践”吗?
你会怎么做?

那么,这里真正的问题是为什么要求删除或更新数据库中不存在的客户? - Steve
1
通常情况下,我的所有删除操作都会返回一个整数,表示已删除的项目数量。更新操作也是如此。您可能需要更新多个项目。按ID更新只是一种特殊的更新方式。仍然可以返回相同的内容。我倾向于不加区分地思考和处理它。但这取决于个人口味。 - Max
所以你会通过业务逻辑将更新、删除的项目数量信息传递到最高层,而不是抛出异常... 是的,这就是我现在正在做的事情,但是使用“true”、“false”... - David Mason
1
当删除或更新获得无效的 ID 时,我会抛出异常。我认为这是避免任何可能发生的问题最简单的方法。当你调用这两个逻辑时,你必须捕捉错误并向用户发送有关他/她做了什么或没有做什么的消息。永远不要让错误到达用户,这是一种安全漏洞。 - turanszkik
3个回答

4
我认为在这些情况下不应该抛出异常——原因如下:
异常应该用于异常情况——主要是代码中无法控制的情况,比如网络连接错误等等。
对于非异常情况抛出异常只会令人恼火,正如Eric Lipprt在这里所解释的那样。
  • 尝试从数据库接收客户信息,但找不到给定id的客户。

这很简单——在数据库中没有找到客户——返回null。这种情况并不特殊,所以没有理由抛出异常。

  • 尝试通过id删除客户,但在数据库中找不到id。

如果在数据库中找到了该客户,则此操作将导致删除该客户。
如果在数据库中没有找到该客户,最终结果与找到该客户时的结果相同,那么你为什么要在意一开始它不存在呢?同样,没有理由抛出异常。

  • 尝试通过id更新客户,但在数据库中找不到id。

这是最棘手的情况,但基本上有两种合法的处理方法:

一种方法是像任何数据库在更新语句中有一个不适合表中任何行的where子句时所做的那样——什么也不做。
至于让客户端知道是否有实际更新或者只是一个空操作,你可以检查影响的行数并返回true/false或客户/ null给客户端。

另一种方式是将更新转换为"upserts"——因此,如果在数据库中找不到客户,则简单地创建一个新的客户。
这也可以使用简单的true/false返回值向客户端表示。在这种情况下,你应该正确命名该方法——例如AddOrUpdateCustomer


这与Max的回答类似。我完全能理解! - David Mason
我基本上同意,但有些情况下我仍然会抛出异常:如果我知道记录必须存在(我事先加载了它们并现在执行某些操作),那么我会抛出异常,因为这真的是意料之外的。 - Mario The Spoon
@MarioTheSpoon 即使在这种情况下,我也不会抛出异常。返回空值的 where 子句是一种常见事件,而不是异常事件 - 有更好的方法向客户端指示某些事情未按预期进行,并且不应将异常用作流程控制。 - Zohar Peled
@ZoharPeled 但是如果在插入操作中元素已经存在,你会抛出异常吗?例如,具有序列号7236的客户已经存在,有人试图插入一个具有此ID的客户。在这种情况下,抛出AlreadyExists异常是否正确? - David Mason
1
@DavidMason 如果序列号应该是唯一的,那么这可能是使用异常的好地方,因为这是一个相当特殊的情况,但更重要的是,当您抛出异常时,调用方法无法忽略它(至少在调用堆栈中的一个方法必须捕获它),但调用方法可以轻松地忽略你的方法返回的任何值 - 在这种情况下,这是更重要的原因,我认为应该抛出异常而不是返回表示成功/失败的值。 - Zohar Peled

1
你不应该使用 异常 作为一种向调用者发出预期执行流的信号的方式。应该在函数返回时返回有意义的值来完成此操作。如果你的函数返回值更复杂,不能通过简单的 true/false 返回来表达,你可以声明一个 enum 作为返回值,甚至在更复杂的情况下使用 Tuple

尝试从数据库中获取客户端,但是给定的 id 的客户端没有找到。

尝试按 id 删除客户端,但是在数据库中找不到该 id。

尝试按 id 更新客户端,但是在数据库中找不到该 id。

所有这三种可能的情况都应该由 DB 很好地处理,并且当出现这样的路径时,只需向调用者返回足够的信息,以便它可以处理结果并适当地采取行动。


0

当出现意外情况时,您必须抛出异常。以您的示例为例:

  1. 如果未找到,则getById调用返回item | null =>不存在错误

  2. repo.Delete函数必须具有有效的id => db函数抛出异常(最好是ArgumentException)...但是控制器/管理器需要捕获(ArgumentException),然后有两个选项:

    2a. 'delete not exist item' => success -> 方法调用后,item不存在

    2b. 例如:控制器向客户端发送错误消息“要删除的项目不存在”

  3. repo.Update函数必须具有有效的id => 如上所述,没有2a选项

如果函数需要此参数 => 抛出异常

如果函数处理'not found=null'情况 => 不抛出异常


ArgumentExceptions是用于无效参数的。在删除操作的情况下,成功(找到并删除客户)和未找到客户的最终结果是相同的 - 为什么要使用异常呢?作为软件的最终客户,我为什么要关心该项是否被删除或一开始就不存在呢? - Zohar Peled
删除函数 => 如果项目已成功删除,但如果未找到该项目,则代码存在错误,例如如果我写错了并将0作为ID传递,没有错误消息,我就会得到一个错误的测试结果(例如)。 - Max94
如果物品本来就不存在,那么在代码中它就没有必要成为一个问题。这里是一个快速演示:假设您拥有一个具有数据网格的网站。两个客户在不同的计算机上查看相同的记录。其中一个从数据库中删除了一个项目,一分钟后,另一个客户端尝试删除相同的项目,但并不知道该项已被删除。第二个用户应该收到错误消息吗? - Zohar Peled
好的,不是错误消息,而是一些消息 => 我向管理员提供一个用户列表,管理员可以删除一个用户(只有一个管理员) 如果管理员传递给我一个不在我提供给管理员的用户列表中的ID => 错误有不同类型的情况,每个人都需要定制行为 - Max94
"抛出异常并捕获它"或者"返回null/false并检查函数的结果"......我认为这两种行为之间有一点小差别,例如对于getById和delete函数。 - Max94
我仍然认为,在100次中至少有99次,看到像“无法删除客户,因为一开始就没有客户”这样的错误消息有点傻。虽然我同意在一些罕见情况下可能需要这样做,但这仍然不是抛出异常的好理由 - 简单的返回值可以用一半的价格完成任务。 - Zohar Peled

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