为什么,您更喜欢例外或返回代码?

时间:2008-09-19 04:43:15

标签: language-agnostic exception

我的问题是大多数开发人员更喜欢错误处理,异常或错误返回代码。请具体说明语言(或语言家族),以及为什么选择其他语言。

我出于好奇而问这个问题。我个人更喜欢错误返回代码,因为它们不那么具有爆炸性,并且如果不想要,也不会强制用户代码支付异常性能损失。

更新:感谢所有答案!我必须说,虽然我不喜欢代码流与异常的不可预测性。关于返回代码(以及他们的兄弟句柄)的答案会给代码添加大量的噪音。

24 个答案:

答案 0 :(得分:96)

答案 1 :(得分:30)

我实际上都使用它们。

如果是已知的可能错误,我会使用返回码。如果这是我知道可能会发生的情况,那么会有一个代码被发回。

例外仅用于我不期望的事情。

答案 2 :(得分:19)

根据框架设计指南:可重用.NET库的约定,惯用法和模式中标题为“例外”的第7章,给出了为什么在OO上使用异常而不是返回值的原因。框架,如C#。

也许这是最令人信服的理由(第179页):

“异常与面向对象的语言很好地集成。面向对象的语言倾向于对非OO语言中的函数强加的成员签名施加约束。例如,在构造函数的情况下,运算符重载和属性,开发人员在返回值中没有选择。因此,无法对面向对象框架的基于返回值的错误报告进行标准化。错误报告方法,例如,例外,这是方法签名的带外是唯一的选择。

答案 3 :(得分:10)

我的偏好(在C ++和Python中)是使用异常。语言提供的工具使其成为一个定义良好的过程,既可以提升,捕获和(如有必要)重新抛出异常,使模型易于查看和使用。从概念上讲,它比返回代码更清晰,因为特定的例外可以通过其名称来定义,并附带其他信息。使用返回码,您只能使用错误值(除非您想要定义ReturnStatus对象或其他内容)。

除非您编写的代码对时间要求严格,否则与展开堆栈相关的开销并不足以让人担心。

答案 4 :(得分:7)

只有在您不期望的事情发生时,才能返回例外情况。

历史上另一个例外点是返回代码本质上是专有的,有时可以从C函数返回0表示成功,有时为-1,或者其中任何一个为失败而1表示成功。即使列举它们,枚举也可能不明确。

例外也可以提供更多的信息,特别是拼写出“错误的东西,这里是什么,堆栈跟踪和上下文的一些支持信息”

话虽这么说,一个列举良好的返回代码对于一组已知的结果非常有用,这是一个简单的“函数结果,它只是这样运行”

答案 5 :(得分:4)

在Java中,我使用(按以下顺序):

  1. 按合同设计(在尝试任何可能失败的之前确保满足前提条件)。这会捕获大多数内容,我会为此返回错误代码。

  2. 处理工作时返回错误代码(如果需要,还可执行回滚)。

  3. 例外情况,但仅使用 来处理意外情况。

答案 6 :(得分:4)

我刚才写了一篇blog post

抛出异常的性能开销不应该在您的决定中起任何作用。毕竟,如果你做得对,一个例外是例外

答案 7 :(得分:4)

我不喜欢返回代码,因为它们会导致以下模式在整个代码中出现

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

很快,一个由4个函数调用组成的方法调用会增加12行错误处理。其中一些调用永远不会发生。如果和切换案件比比皆是。

如果您使用它们,那么例外情况会更加清晰......表示异常事件......之后执行路径无法继续。它们通常比错误代码更具描述性和信息性。

如果在方法调用之后有多个状态应该以不同方式处理(并且不是例外情况),请使用错误代码或输出参数。虽然Personaly我发现这很罕见..

我对“性能惩罚”的反驳有所了解..更多的是在C ++ / COM世界中,但在较新的语言中,我认为差别并不大。在任何情况下,当某些事情爆发时,性能问题会降级为后备者:)

答案 8 :(得分:3)

对于任何体面的编译器或运行时环境,异常都不会产生重大损失。它或多或少像跳转到异常处理程序的GOTO语句。此外,运行时环境(如JVM)捕获的异常有助于更容易地隔离和修复错误。我会在任何一天用C语言中的段错误在Java中使用NullPointerException。

答案 9 :(得分:3)

几乎每次返回代码都无法通过“Pit of Success”测试。

  • 很容易忘记检查返回代码,然后再发生红鲱鱼错误。
  • 返回代码没有任何关于它们的好调试信息,如调用堆栈,内部异常。
  • 返回代码不会传播,与上述内容一起,往往会驱动过多和交织的诊断日志记录,而不是登录到一个集中的位置(应用程序和线程级异常处理程序)。
  • 返回代码往往会以嵌套的“if”块
  • 的形式驱动凌乱的代码
  • 开发人员花在调试未知问题上的时间本来就是一个明显的例外(成功的障碍)是很昂贵的。

关于表现:

  • 异常可能在计算上相当昂贵相对于根本不投掷,但出于某种原因它们被称为EXCEPTIONS。速度比较总是设法假设100%的异常率,而这种情况绝对不是这种情况。即使异常速度慢了100倍,如果它仅在1%的时间内发生,那真正重要的是多少?
  • 除非我们讨论图形应用程序的浮点运算或类似的东西,否则与开发人员时间相比,CPU周期便宜。
  • 从时间角度来看,成本具有相同的论点。相对于数据库查询或Web服务调用或文件加载,正常的应用程序时间将使异常时间相形见绌。例外几乎是sub-MICROsecond in 2006
    • 我敢在.net中工作的任何人,设置你的调试器来打破所有异常,只禁用我的代码,看看有多少异常已经发生,你甚至都不知道。

答案 10 :(得分:3)

我从实用程序员得到的一条很好的建议是“你的程序应该能够执行所有主要功能而不使用任何异常”。

答案 11 :(得分:3)

异常不适用于错误处理,IMO。例外就是这样;你没想到的特殊事件。请谨慎使用我说。

错误代码可以正常,但从方法返回404或200是不好的,IMO。使用枚举(.Net)代替,使代码更易读,更易于用于其他开发人员。此外,您不必在数字和描述上维护表格。

也; try-catch-finally模式在我的书中是一种反模式。尝试 - 终于可以是好的,尝试捕获也可以是好的,但尝试捕获 - 终于永远不会好。 try-finally通常可以被“using”语句(IDispose模式)替换,这是更好的IMO。而实际捕获你能够处理异常的Try-catch是好的,或者如果你这样做:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

所以只要你让异常继续冒泡就可以了。另一个例子是:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

这里我实际上处理了异常;当更新方法失败时我在try-catch之外做的是另一个故事,但我认为我的观点已经完成。 :)

为什么然后尝试捕捉 - 终于反模式?原因如下:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

如果db对象已经关闭会怎么样?抛出一个新的异常并且必须处理它!这样更好:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

或者,如果db对象未实现IDisposable,请执行以下操作:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

无论如何,这是我的2美分! :)

答案 12 :(得分:3)

我相信返回代码会增加代码噪音。例如,由于返回代码,我总是讨厌COM / ATL代码的外观。必须对每行代码进行HRESULT检查。我认为错误返回代码是COM架构师做出的错误决定之一。这使得难以对代码进行逻辑分组,因此代码审查变得困难。

当每行显式检查返回码时,我不确定性能比较。

答案 13 :(得分:3)

我在异常和非异常情况下都在python中使用Exceptions。

通常可以使用Exception来指示“无法执行请求”,而不是返回Error值。这意味着你/总是/知道返回值是正确的类型,而不是任意的None或NotFoundSingleton等。这是一个很好的例子,我更喜欢使用异常处理程序而不是返回值的条件。

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

副作用是当运行datastore.fetch(obj_id)时,您永远不必检查其返回值是否为None,您可以立即免费获得该错误。这与论点相反,“你的程序应该能够执行所有主要功能而不使用异常”。

这是另一个例子,其中异常“特别”有用,以便编写处理不受竞争条件影响的文件系统的代码。

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

一个系统调用而不是两个,没有竞争条件。这是一个很糟糕的例子,因为很明显这会因为OSError在更多情况下失败而不是目录不存在,但对于许多严格控制的情况来说,这是一个“足够好”的解决方案。

答案 14 :(得分:2)

有很多理由更喜欢Exceptions而不是返回代码:

  • 通常,为了便于阅读,人们会尝试最小化方法中的return语句数。这样做,异常会阻止在非合并状态下执行一些额外的工作,从而防止可能损坏更多数据。
  • 异常通常比返回值更容易扩展,更易于扩展。假设一个方法返回自然数,并且当发生错误时使用负数作为返回码,如果你方法的范围改变并且现在返回整数,你将不得不修改所有方法调用而不是仅仅调整一点例外。
  • 例外允许更容易分离正常行为的错误处理。它们允许确保某些操作以某种方式执行原子操作。

答案 15 :(得分:2)

一个很大的区别是异常会强制您处理错误,而错误返回代码可能会被取消选中。

错误返回代码,如果大量使用,也会导致非常难看的代码,如果测试类似于这种形式:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

就个人而言,我更喜欢对调用代码应该或必须对其执行的错误使用异常,并且只使用错误代码来表示“预期失败”,其中返回的内容实际上是有效且可能的。

答案 16 :(得分:2)

我更喜欢使用异常进行错误处理并返回值(或参数)作为函数的正常结果。这提供了一种简单而一致的错误处理方案,如果正确完成,它可以使代码更清晰。

答案 17 :(得分:1)

我没有发现返回代码比异常更难看。除了try{} catch() {} finally {}之外,您有if(){}的返回码。我过去常常因为帖子中的原因而担心例外;你不知道指针是否需要清除,你有什么。但是我认为在返回代码方面你遇到了同样的问题。除非您了解有关函数/方法的一些详细信息,否则您不知道参数的状态。

无论如何,如果可能,您必须处理错误。您可以轻松地将异常传播到顶层,因为忽略返回代码并让程序发生段错误。

我喜欢为结果返回值(枚举?)和异常情况的异常。

答案 18 :(得分:1)

我有一套简单的规则:

1)使用返回码代表您希望直接来电者做出反应的事情。

2)对范围更广的错误使用异常,并且可以合理地预期由调用者之上的许多级别处理,以便错误的意识不必渗透到多层,使代码更复杂

在Java中我只使用未经检查的异常,检查异常最终只是另一种形式的返回代码,根据我的经验,方法调用可能“返回”的二元性通常更多的是阻碍而不是帮助。

答案 19 :(得分:1)

我在异常与返回代码参数中的一般规则:

  • 当您需要本地化/国际化时使用错误代码 - 在.NET中,您可以使用这些错误代码来引用资源文件,然后以适当的语言显示错误。否则,请使用例外
  • 仅对真正例外的错误使用例外。如果它经常发生,可以使用布尔值或枚举错误代码。

答案 20 :(得分:1)

我担心异常的一件事是抛出异常会搞砸代码流。例如,如果你这样做

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

或者甚至更糟的是,如果我删除了一些我不应该拥有的内容,但是在我完成剩余的清理工作之前就抓住了。投掷给很差的用户恕我直言。

答案 21 :(得分:1)

我只使用例外,没有返回代码。我在这里谈论Java。

我遵循的一般规则是,如果我有一个名为doFoo()的方法,那么如果它不是“执行foo”,那么就会发生异常并且应该抛出异常。

答案 22 :(得分:0)

对于像Java这样的语言,我会使用Exception,因为如果不处理异常,编译器会给出编译时错误。这会强制调用函数处理/抛出异常。

对于Python,我更加矛盾。没有编译器,因此调用者可能无法处理导致运行时异常的函数抛出的异常。如果使用返回代码,如果处理不当,则可能会出现意外行为,如果使用异常,则可能会遇到运行时异常。

答案 23 :(得分:0)

我通常更喜欢返回码,因为它们使呼叫者确定失败是否是异常

这种方法在Elixir语言中很常见。

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

人们提到返回代码可以使您有很多嵌套的if语句,但是可以使用更好的语法来处理。在Elixir中,with语句使我们可以轻松地从任何失败中分离出一系列快乐路径返回值。

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir仍然具有引发异常的功能。回到我的第一个示例,如果文件无法写入,我可以执行上述任一操作来引发异常。

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

如果作为调用者的我知道如果写入失败要提出错误,则可以选择调用File.write!而不是File.write。 或者,我可以选择致电File.write并以不同方式处理每种可能的失败原因。

当然,如果需要,总是可以rescue例外。但是与处理信息丰富的返回值相比,对我来说似乎很尴尬。如果我知道某个函数调用可能失败甚至应该失败,那么它的失败就不是一种例外情况。