当检索方法无法生成返回值时,应该返回“null”还是抛出异常?

551

我正在使用Java语言,我有一个方法,如果找到了对象,就应该返回它。

如果没有找到,我应该:

  1. 返回null
  2. 抛出异常
  3. 其他

哪种是最佳实践或习惯用法?


65
无论你做什么,一定要记录下来。我认为这一点比哪种方法是“最好”的更重要。 - Rik
7
这取决于所使用的编程语言中流行的习语。请在问题上打上相应的编程语言标签。 - Teddy
3
返回 null 可能仅表示成功或失败,这通常并不能提供足够的信息(有些方法可能以多种方式失败)。 库应该更好地抛出异常,使错误明确化,这样主程序可以在更高的层次上决定如何处理错误(相对于内置错误处理逻辑而言)。 - 3k-
5
在我看来,真正的问题是我们是否应该认为找不到实体是例外情况,如果是,为什么?没有人真正回答如何得出这个结论,现在问答关闭了。这个行业还没有在这个重要的话题上达成共识,真是遗憾。是的,我知道这取决于情况。因此,请解释为什么会有这种情况,不只是简单地说“如果是例外情况,就排除掉”。 - crush
36个回答

528

如果您总是期望找到一个值,而它确实缺失,则抛出异常。异常表示出现了问题。

如果该值可能存在也可能不存在,并且对应用程序逻辑都是有效的,则返回 null。

更重要的是,您在代码其他地方要怎么做呢?一致性很重要。


31
@Ken:+1,如果您确实抛出了异常,但可以事先检测到它(例如HasItem(...)),那么用户应该提供所述的Has*或Contains方法。 - user7116
4
考虑使用 Maybe<T> 来代替返回值或 null,以表达数值缺失的情况。详见 http://mikehadlow.blogspot.nl/2011/01/monads-in-c-5-maybe.html。 - user2609980
2
从Java 8开始,你也可以返回一个Optional<T>,这是一种@ErwinRooijakkers的设计选择方式。 - José Andias
5
我发现每个答案都回应了同样的谚语:“如果它是例外,就抛弃。”大多数工程师都会熟悉这个原则。在我看来,真正的问题在于如何确定是否应该被视为例外情况。原帖作者正在寻找例如存储库模式之类的最佳实践。例如,如果给定一个主键,对象不存在通常被认为是例外情况吗?是的,这是他的领域所确定的问题,但是大多数经验丰富的专家建议什么呢?这就是我们应该看到的答案类型。 - crush
6
我认为一个指导原则是传递给函数的参数。如果传递一个标识,比如你提到的主键,那么如果系统中不存在该项,则应该被视为异常情况,因为这表明了系统的不一致状态。例如:GetPersonById(25) 如果该人已被删除,则会引发异常,但是 GetPeopleByHairColor("red") 则会返回空结果。所以,我认为参数反映了预期的某些方面。 - John Knoop
显示剩余4条评论

111
只有在真正出现错误的情况下才抛出异常。如果对象不存在是预期行为,则返回null。 否则,这只是一种偏好。

4
我不同意。你可以将异常作为状态码抛出:“NotFoundException”。 - ACV
这绝对不应该是个人喜好的问题。如果不是在你的团队代码中,那么当将其他开发者的代码与你自己的代码(外部库)交织在一起时,几乎肯定会出现不一致的代码。 - crush
8
我认为处理空值情况比“NotFoundException”容易得多。想想每次检索请求抛出“NotFoundException”,你需要编写多少行try-catch代码……在我看来,读那些代码让人很痛苦。 - visc
2
我想引用Tony Hoare的话:“我把它称为我的十亿美元错误”。我不会返回null,要么抛出异常并正确地处理它,要么返回一个空对象。 - seven
@visc 不要捕获你无法解决的异常!如果这是一个错误的情况,请将其传播到顶层并失败请求/ UI操作。最佳实践是经常抛出,很少捕获。https://literatejava.com/exceptions/ten-practices-for-perfect-java-exception-handling/ - Thomas W

85

一般原则是,如果方法总是应该返回一个对象,则使用异常。如果您预计偶尔会出现null并希望以某种方式处理它,则使用null。

无论做什么,我强烈反对第三个选项:返回一个字符串,上面写着"WTF"(什么鬼)。


加一,因为在很多年前我做过几次这样的“临时”修复……并不是一个好主意。特别是如果你是学生,并且代码将被审核。 - rciafardone
24
我本来想给一个差评,因为“WTF”选项对我来说看起来很棒......但显然我还是有一颗善良的心。 - swestner
20
抛出新的 WtfException。 - Kamafeather
我认为如果这个答案讨论了一个方法总是返回一个对象而不是“偶尔返回null”的原因会很有帮助。什么情况下需要这种类型的情况?请举一个这样的情况的例子。 - crush

52

如果 null 从不表示错误,则只需返回 null。

如果 null 总是表示错误,则抛出异常。

如果 null 有时表示异常,则编写两个例程。一个例程会抛出异常,另一个例程是一个布尔测试例程,它将该对象放在输出参数中,并且如果未找到该对象,则该例程返回 false。

使用 Try 例程很难误用。忘记检查 null 非常容易。

因此,当 null 表示错误时,只需编写

object o = FindObject();

当空值不是错误时,您可以编写类似以下的代码

if (TryFindObject(out object o)
  // Do something with o
else
  // o was not found

1
如果C#提供了真正的元组,那么这将是一个更有用的建议,这样我们就可以避免使用[out]参数。尽管如此,这仍然是首选模式,所以+1。 - Erik Forbes
3
在我看来,“Try”方法是最好的方法。你不需要再查找如果对象无法返回会发生什么。使用“Try”方法,你可以立即知道该怎么做。 - OregonGhost
让我想起了Laravel Eloquent中的findfindOrFail - javier_domenech
@ErikForbes 我知道你的评论已经有些年头了,但是难道答案不应该是定义一个多属性对象,然后从 TryFindObject 方法中返回它吗?元组似乎更像是一种懒惰的编程范式,适用于那些不想花时间定义封装多个值的对象的程序员。本质上,这就是元组的全部内容。 - crush
@crush - 命名元组字面量是一种选择,我个人认为。这是一个带有元组的异步 Try Get 模式链接。https://dev59.com/4XI-5IYBdhLWcg3w48xP#51565145 - ttugates

27

我想简要总结一下之前提到的选项,并提出一些新的选项:

  1. 返回null
  2. 抛出异常
  3. 使用null对象模式
  4. 为您的方法提供布尔参数,以便调用者可以选择是否要抛出异常
  5. 提供额外的参数,以便调用者可以设置一个在没有找到值时返回的值

或者您可以将这些选项组合起来:

提供多个重载版本的getter方法,以便调用者可以决定采取哪种方式。在大多数情况下,只有第一个版本包含搜索算法的实现,其他版本只是围绕第一个版本进行封装:

Object findObjectOrNull(String key);
Object findObjectOrThrow(String key) throws SomeException;
Object findObjectOrCreate(String key, SomeClass dataNeededToCreateNewObject);
Object findObjectOrDefault(String key, Object defaultReturnValue);

即使您选择只提供一个实现,您可能希望使用这样的命名约定来澄清您的合同,这有助于您决定添加其他实现时。

您不应过度使用它,但在编写将在许多不同应用程序中使用许多不同错误处理约定的帮助器类时,它可能会很有用。


我喜欢这些清晰的函数名称,特别是orCreate和orDefault。 - marcovtwout
5
大部分可以用Expected<T> findObject(String)更加简洁地编写,其中Expected<T>包含以下函数:orNull()orThrow()orSupplied(Supplier<T> supplier)orDefault(T default)。这种方法将数据获取与错误处理分离。 - WorldSEnder
直到现在我才知道Expected<T>。它似乎很新,可能在我写原始答案时还不存在。也许你应该把你的评论变成一个正式的答案。 - Lena Schimmel
此外,Expected<T> 是一个 C++ 模板。其他面向对象语言中是否也有它的实现? - Lena Schimmel
在Java 8中,返回Optional<T>(在其他语言中称为Maybe<T>等)也是一种选择。这清楚地告诉调用者返回空值是一种可能性,并且如果调用者没有处理该可能性,则不会编译,而null则不同,在Java中即使调用者没有检查它,也会编译。 - Some Guy

20

使用空对象模式或抛出异常。


我简直不敢相信这个答案还没有被投到最高点。这才是真正的答案,而且任何一种方法都非常简单,可以节省大量代码膨胀或NPE。 - Bane
http://sourcemaking.com/design_patterns/null_object - Bane
3
如果使用空对象模式,如何区分键映射到空对象和键没有映射的情况?我认为返回无意义的对象比返回null要糟糕得多。对于未准备好处理null的代码返回null通常会导致抛出异常。虽然不是最优选择的异常,但仍然会产生异常。返回无意义对象更可能导致代码错误地将无意义数据视为正确数据。 - supercat
3
对于实体查找,空对象模式会有怎样的行为?例如 Person somePerson = personRepository.find("does-not-exist"); 这个方法返回ID为 does-not-exist 的空对象。那么对于 somePerson.getAge(),正确的行为是什么?目前为止,我还没有被说服认为空对象模式是解决实体查找问题的正确方案。 - Abdull
这就是为什么我说要么抛出异常,如果在您的情况下传播NullPerson没有意义。实际上,我认为对于实体查找,最好的方法是抛出异常,然后您的控制器/服务/模型/任何其他东西将捕获它并决定是否重新抛出或返回空对象。但是,是的,您的数据访问层应该抛出异常,异常比想知道链中哪里有空值更容易捕获/调试。 - pmlarocque
显示剩余5条评论

18

抛出异常的优点:

  1. 在调用代码中具有更加清晰的控制流。检查空值注入一个条件分支,这个分支可以通过try/catch来处理。检查null并不能说明你正在检查什么 - 你是因为期望错误而检查null,还是为了不将其向下传递而检查null?
  2. 消除了"null"含义的歧义性。当你只有一件事可以基于时,null是否代表一个错误或者实际上存储在值中是不清楚的。
  3. 提高应用程序方法行为之间的一致性。通常情况下,异常会暴露在方法签名中,因此您可以更好地理解应用程序方法所考虑的边缘情况以及应用程序可以以可预测的方式做出反应的信息。

更多解释和示例,请参见:http://metatations.com/2011/11/17/returning-null-vs-throwing-an-exception/


2
+1 是因为第二点非常好 - null 不具有与未找到相同的含义。当处理动态语言时,这变得更加重要,因为 Null 实际上可能是函数存储/检索的对象。 - Adam Terrey
1
对于第二点加1。null是错误吗?null是否存储给定键?null是否表明该键不存在?这些是真正的问题,这使得返回null几乎从来不正确。这很可能是最好的答案,因为其他人只是模棱两可地说“如果它是异常,请抛出”。 - crush
异常可能会导致性能下降,与返回值相比(例如在Java中)。而未经检查的异常(例如在Java中)可能会出现在意想不到的地方。 - tkruse

13

保持使用的API的一致性。


13

这取决于你的编程语言和代码是否采用:

  • LBYL (先看后跳):它建议检查值(因此返回null)
  • EAFP (请求宽恕比获得许可更容易):它建议直接尝试操作并观察是否失败(抛出异常)

尽管我同意上述观点……但异常应该用于异常/错误情况,使用检查时最好返回null。


Python中的EAFP vs. LBYL:
http://mail.python.org/pipermail/python-list/2003-May/205182.html (网络档案)


3
在某些情况下,EAFP是唯一有意义的方法。例如,在并发映射/字典中,没有办法在请求映射时询问它是否存在。 - supercat

13

只需问自己:“对象未被找到是否是一个例外情况?”如果在程序的正常流程中预计会发生这种情况,那么您可能不应该引发异常(因为这不是异常行为)。

简而言之:使用异常来处理异常行为,而不是处理程序的正常控制流程。

-艾伦。


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