抛出exception来控制流程 – 代码味道?
考虑一下这段代码(特别是Java):
public int doSomething() { doA(); try { doB(); } catch (MyException e) { return ERROR; } doC(); return SUCCESS; }
doB()
定义为:
private void doB() throws MyException
基本上, MyException
仅在doB()
满足某些条件(这不是灾难性的,但确实需要以某种方式提高此条件)的情况下存在,以便doSomething()
知道退出时出错。
您是否发现使用exception,在这种情况下控制流量,可以接受? 或者这是代码味道? 如果是这样,你会如何重构这个?
这完全取决于错误条件是什么,以及方法的工作是什么。 如果返回ERROR
是一种处理调用函数错误的有效方法,为什么它会变坏?
然而,通常它是一种气味。 考虑一下:
bool isDouble(string someString) { try { double d = Convert.ParseInt32(someString); } catch(FormatException e) { return false; } return true; }
这是一个非常大的代码味道,因为你不期望双重值。 您只想知道字符串是否包含double。
有时,您使用的框架没有其他方法可以执行您想要的操作。 对于上述情况,有一种更好的方法:
bool isDouble(string someString) { bool success; Convert.TryParseInt32(someString, ref success); return success; }
这种例外有一个特殊的名字,由我最近阅读的博客创造。 但遗憾的是,我忘记了它的名字。 如果你知道的话请评论。 最后但并非最不重要的是,上面是伪代码。 我不是ac#developer所以上面的代码不能编译,我敢肯定,但是TryParseInt32 / ParseInt32certificate了这一点我认为我会选择C#。
现在,到您的代码。 让我们检查两个function。 一个闻起来,另一个没有:
气味
public int setupSystem() { doA(); try { doB(); } catch (MyException e) { return ERROR; } doC(); return SUCCESS; }
这是代码味道 ,因为当您想要设置系统时,您不希望它失败。 未能设置系统意味着您无法在不处理该错误的情况下继续操作。
好的
public int pingWorkstation() { doA(); try { doB(); } catch (MyException e) { return ERROR; } doC(); return SUCCESS; }
没关系,因为该方法的目的是测试工作站是否仍可访问。 如果不是,那么这是该方法结果的一部分,而不是需要替代返回路径的特殊情况。
在doB()失败时执行doC()真的很重要吗? 如果没有,为什么不简单地让Exception在堆栈中向上传播到可以有效处理的位置。 就个人而言,我考虑使用错误代码代码气味。
编辑:在您的评论中,您已经准确地描述了您应该简单声明的情节
public void doSomething() throws MyException
我对OP代码的唯一问题是你混合范例 – doB通过抛出exception显示错误,而doSomething通过返回代码显示错误。 理想情况下,你会选择其中一个。 当然,在传统维护中,您可能没有那么奢侈。
如果您选择返回错误代码,那没关系,但我不喜欢它,因为它鼓励您使用辅助通道(如全局变量)在失败时通信状态,而不是将该状态捆绑到exception并让它冒泡到堆栈直到您可以做些什么。
我从来不喜欢使用控制流的exception(在某些语言中,比如CLR,它们很贵)。
如果你可以修改doB() ,最好的办法是改变它以返回一个表示成功或失败的布尔值,所以你的例子看起来像:
public int doSomething() { doA(); if (!doB()) { return ERROR; } doC(); return SUCCESS; }
在以下情况下应使用例外:
- 一个函数无法正常完成,并且
- 当没有可用于指示失败的返回值时,或
- 当抛出的exception传达的信息多于
return FAILURE;
时的信息return FAILURE;
例如,可以嵌套exception等。
请记住,除了返回值或方法参数之外,所有exception都只是在代码的不同部分之间发送的消息。 努力优化通过这些方法传达的信息与API的简单性之间的平衡。 当只需要一个简单的SUCCESS / FAILURE标志(并且该方法不需要返回其他值)时,请使用它。 如果方法已经必须返回一个值,那么通常需要使用一个exception(这是一种查看它的方法,只是方法中的“exception”返回值)。 如果必须传递的故障信息太丰富而无法在返回值中传递(例如,I / O故障的原因),请使用exception。
最后,error handling是一个设计决策,并且没有一套适合所有情况的规则。
使用exception来控制流量 ,绝对是不好的。 只有在特殊情况下才应抛出例外情况。
但是,使用一种简单地抛出exception或什么都不做的实用方法并不是特别糟糕。 例如,您可能希望validation进入方法的所有参数,或检查内部状态在复杂计算之间是否仍然有效(由于某种原因,您无法在内部强制执行,也许您正在接受闭包并执行它们)。 在这些情况下,将一致性检查提取到自己的方法中,以提高“实际”方法实现的可读性并不是一个坏主意。
分界线实际上是当遇到超出正常操作参数的东西时应该抛出exception,并且事情是如此糟糕以至于该方法没有真正的方法来继续它的工作。
作为不该做的一个例子,这将使用流控制的exception:
public int calculateArraySize(Object[] array) { int i = 0; try { array[i++]; } catch (ArrayIndexOutOfBoundsException ignore) {} return i; }
我相信它会返回正确的结果,但它会非常缓慢和低效,并且对于习惯于正常使用exception的人来说也很难阅读。 😉
另一方面,我认为这样的事情会好的:
public void myMethod(int count, Foobar foo) throws MyPackageException { validateArgs(count, foo); // Stuff } private void validateArgs(int count, Foobar foo) throws MyPackageException { if (m_isClosed) { throw new IllegalStateException("Cannot call methods on a closed widget"); } if (count < 0) { throw new IllegalArgumentException("count must be greater than zero"); } foo.normalise(); // throws MyPackageException if foo is not fully initialised at this point }
尽管所有第二种方法都可能引发exception,但它并没有这样做来控制程序流程,而是为了响应特殊情况而提高它们。
如果出现错误,可以抛出exception,就像在doB()中一样。 但问题是函数doSomething()。
您不应该使用return语句指示成功或失败。 你应该做:
try { doB(); } catch (MyException e) { throw MyException2(e); }
不应仅使用例外来控制流量。 应该使用经过检查的exception来告诉调用代码不符合API合同的某些条件。 我不会设计doSomething
来处理通常使用try/catch
块调用doB
失败的情况。 如果经常出现故障需要处理,我会设计doB
来返回成功或失败的boolean
以指示其调用方法是否继续使用自己的doC
类型方法:
public int doSomething() { doA(); if ( doB() ) doC(); return SUCCESS; } else { return ERROR; } }
根据doB的逻辑,你可以有一些返回值指向它是否正常,然后doSomething可以使用返回的值以适当的方式处理这种情况。
例外情况是非标准行为。 所以可以使用它们来控制流量。
正如Sun所说:
一般来说,不要抛出RuntimeException或创建RuntimeException的子类,因为您不希望因为指定方法可以抛出的exception而烦恼。
这是底线指南:如果可以合理地期望客户端从exception中恢复,则将其作为已检查的exception。 如果客户端无法执行任何操作以从exception中恢复,请将其设置为未经检查的exception。
只记得不要将RuntimeException子类化,而将Exception子类化为更快。
我认为使用exception非常简单,简单明了。 我会创建返回正常值的常规函数,并为它们的设计保留exception。
正如其他人所指出的,它实际上取决于这里涉及的方法的预期function。 如果doSomething()
的作用是检查某些东西,返回一个允许正常执行其调用方法的代码继续(虽然在不同的路径上取决于返回的代码)然后让它返回该int可能没问题。 但是,在这种情况下, doB()
应该是一致的,并返回一个布尔值或doSomething()
可以检查的东西,而不是抛出一个不会真正做任何其他事情的exception。
如果doB()
是另一个类的公共方法,可能在doSomething()
之外有其他用途(而不是你指定的同一个类中的私有方法),它会抛出exception,你想在doSomething()
使用它doSomething()
(具有上述相同的作用),那么你的例子就可以了。
事实上,当doB()
抛出exception时返回的代码称为ERROR
,但是,表明它可能是一个失败,它会阻止doSomething()
完成的任何操作。 在这种情况下,您可能不应该捕获doB()
抛出的exception,只是让它被抛出堆栈,直到它到达可以处理它的点,或者直到它到达可以报告/记录它的一般error handling程序或者随你。
在C#中,抛出exception相对昂贵,性能方面 – 因此避免将其用于流控制的另一个原因。 这是Java的情况吗? 是的,有一个争论是要提前不要过度分析性能。 但是,如果你知道某些东西需要额外的时间,而且很容易避免,你不应该这样做吗? 除非你真的不在乎!
请参阅此问题的最佳答案:
Javaexception有多慢?
事实certificate它们至少比普通代码流慢50倍。 所以我会说在通常的代码运行中使用exception绝对是个坏主意。
以下是我将如何处理它:
public void doSomething() throws MyException{ doA(); try { doB(); } finally { doC(); } }
如果在doB()中发生错误,则exception将在堆栈中向上传播。
你们这里杀了我 你永远不想使用exception来控制流量。 与在任何地方使用goto语句没有什么不同!
例外是例外,意味着发生了一些未预料到的事情。 这么多东西,所以我们停止了应用程序的执行。 任何曾经在应用程序上工作的人都是这种类型的exception流程控制,它会想要追捕那些做这件事的开发人员并限制他(或她)。
使用exception作为流控制使维护和新function开发成为一场噩梦!