成功时返回true / false与void的函数,失败时抛出异常


22

我正在构建一个API,一个上传文件的函数。如果文件上传正确,此函数将不返回任何内容/无效,并且在出现问题时将引发异常。

为什么要例外而不是错误?因为在异常中,我可以指定失败的原因(无连接,文件名丢失,密码错误,文件描述丢失等)。我想构建一个自定义异常(带有一些枚举来帮助API用户处理所有错误)。

这是一个好习惯还是返回一个对象(内部包含布尔值,可选错误消息和错误枚举)更好?



59
正确与错误相处得很好。无效和例外相处得很好。真实与例外像猫和税一样走到一起。有一天我可能正在阅读您的代码。请不要对我这样做。
candied_orange

4
我非常喜欢返回包含成功或失败的对象的模式。例如,锈病Result<T, E>这里T是结果,如果成功,E如果不成功是错误。抛出异常(在Java中可能不是很确定)会很昂贵,因为它涉及展开调用堆栈,但是创建Exception对象很便宜。显然,请遵循所使用语言的既定模式,但不要将布尔值和此类异常结合在一起。
Dan Pantry

4
小心点 您可能会掉进兔子洞。当您的程序能够处理问题时,通常只需检查先决条件就可以更轻松地检测和处理问题。您很少会捕获异常,在代码中检查其数据,然后在解决问题后重试。因此,拥有非常多的异常类型很少有价值。如果您尝试记录问题,那么这样做会更有意义,但是不要期望编写大量包含大量代码的catch块。
jpmc26 2013年

4
@DanPantry在Java中,在创建异常时(而不是在引发异常时)检查调用堆栈。fillInStackTrace();在类的超级构造函数中被调用Throwable
Simon Forsberg

Answers:


35

引发异常只是使方法返回值的另一种方法。调用方可以像捕获异常一样轻松地检查返回值并进行检查。因此,在之间做出决定throwreturn需要其他条件。

如果抛出异常会危及程序的效率,则通常应避免抛出异常(构造异常对象和展开调用堆栈对计算机而言,不仅仅是将值压入其中而已)。但是,如果您的方法的目的是上传文件,那么瓶颈始终将是网络和文件系统I / O,因此优化return方法毫无意义。

对于应该是简单控制流的异常(例如,通过找到其值进行搜索成功时)抛出异常也是一个坏主意,因为这违反了API用户的期望。但是一种无法实现其目的的方法一个例外情况(或者至少应该如此),因此我认为没有理由不抛出异常。而且,如果这样做,您最好将其设置为自定义的,信息量更大的异常(但最好将其作为标准的更通用异常的子类,例如IOException)。


7
如果文件上传请求失败是预期结果,那么我会惊讶地抛出一个异常(尤其是方法签名中有返回值)。
hoffmale

1
请注意,使其扩展为标准异常的原因是,许多(可能甚至是大多数)调用者都不会编写代码来尝试解决该问题。结果,大多数调用者将需要一个简单的“捕获大量异常”,而不必明确列出所有异常。
jpmc26 2013年

4
@gbjbaanb这就是为什么在确实存在某些无法解决的情况时应使用异常的原因。当您尝试打开文件而无法读取该文件时,这是一个很好的例外情况。当您检查文件是否存在时,需要使用布尔值,因为很可能文件可能不存在。
马尔科姆

21
Kilian在递归的口号中说,“例外是针对特殊情况”,特殊情况是指功能无法实现其目的。如果应该使用该功能读取文件,而无法读取该文件,则异常。如果该函数告诉您文件是否存在(不要担心可能的TOCTOU),那么不存在的文件也不例外,因为该函数仍可以执行应做的事情。如果该函数断言一个文件存在,则该文件不存在是例外,因为这就是“断言”的含义。
史蒂夫·杰索普

10
请注意,告诉您文件是否存在的函数与断言文件存在的函数之间的唯一区别是,不存在文件的情况是否特殊。人们有时会说这是错误条件的一个属性,无论它是否“异常”。它不是错误条件的属性,而是错误条件及其发生的目的的组合属性。否则,我们将永远不会捕获异常。
史蒂夫·杰索普

31

true如果您不false失败,那绝对没有理由成功。客户代码应该是什么样?

if (result = tryMyAPICall()) {
    // business logic
}
else {
    // this will *never* happen anyways
}

在这种情况下,调用者仍然需要一个try-catch块,但是他可以更好地编写:

try {
    result = tryMyAPICall();
    // business logic
    // will only reach this line when no exception
    // no reason to use an if-condition
} catch (SomeException se) { }

因此,true返回值与调用者完全无关。因此,只需保留该方法即可void


通常,有三种设计故障模式的方法。

  1. 返回true / false
  2. 使用 void,引发(检查)异常
  3. 返回中间结果对象。

返回 true /false

在一些较旧的,大多数为c样式的API中使用。不利的一面是,您不知道出了什么问题。PHP经常这样做,导致这样的代码:

if (xyz_parse($data) === FALSE)
   $error = xyz_last_error();

在多线程上下文中,这甚至更糟。

引发(检查)异常

这是一个很好的方法。在某些时候,您会期望失败。Java通过套接字来实现。基本假设是调用应该成功,但是每个人都知道某些操作可能会失败。套接字连接在其中。因此,调用者被迫处理失败。这是一个不错的设计,因为它可以确保调用方实际处理了故障,并为调用方提供了一种处理故障的优雅方法。

返回结果对象

这是处理此问题的另一种好方法。它通常用于解析或仅用于需要验证的事物。

ValidationResult result = parser.validate(data);
if (result.isValid())
    // business logic
else
    error = result.getvalidationError();

调用者的逻辑也很好。

何时使用第二种情况以及何时使用第三种情况尚有争议。有些人认为例外应该是例外,并且您在设计时不应考虑例外的可能性,并且几乎总是使用第三个选项。没事。但是我们已经检查了Java中的异常,因此我认为没有理由使用它们。当基本假设是调用应该成功(例如使用套接字)时,我会使用检查后的expetions ,但是可能会失败,并且当调用应该调用(例如验证数据)是否很不清楚时,我会使用第三个选项。但是对此有不同的意见。

就您而言,我会选择void+ Exception。您期望文件上传成功,如果成功,那就例外了。但是,调用方被迫处理该故障模式,并且您可以返回一个异常,该异常正确地描述了发生了哪种错误。


5

这实际上归结为失败是异常的还是预期的

如果您通常不希望看到错误,那么API用户很可能只是在没有任何特殊错误处理的情况下调用您的方法。引发异常可以使堆栈冒泡到引起注意的位置。

另一方面,如果错误很常见,则应针对正在检查错误的开发人员进行优化,并且try-catch子句比if/elifor或switch系列更麻烦。

始终以使用API​​的人的心态来设计API。


1
就我而言,就像有人指出的那样,应该是API用户无法上传文件的异常失败。
Accollat​​ivo

4

如果您从不打算返回布尔值,请不要返回布尔值false。只需使方法void和文档证明IOException在失败时将引发异常(合适)。

这样做的原因是,如果您确实返回了布尔值,则API的用户可能会得出结论,他/她可以这样做:

if (fileUpload(file) == false) {
    // Handle failure.
}

那当然行不通;也就是说,您的方法的合同与其行为之间存在不一致之处。如果抛出一个检查的异常,则该方法的用户必须处理失败:

try {
    fileUpload(file);
} catch (IOException e) {
    // Handle failure.
}

3

如果您的方法具有返回值,则引发异常可能会使其用户感到惊讶。如果我看到一个方法返回一个布尔值,我很确信如果成功将返回true,否则返回false,并且我将使用if-else子句构造代码。如果您的异常仍然基于枚举,则最好返回枚举值(类似于Windows HResults),并在方法成功时保留枚举值。

当方法不是过程的最后一步时,方法抛出异常也很烦人。必须将try-catch和将控制流写入catch块是意大利面的好食谱,应避免。

如果您要处理异常,请尝试返回void,如果没有异常抛出,用户会发现它成功,并且没有人会尝试使用if-else子句来转移控制流。


1
枚举只是一个想法,可以帮助API用户处理不同类型的错误而不解析错误消息。因此,从我的角度来看,最好返回枚举并为“确定”添加枚举。
Accollat​​ivo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.