强行退出递归的抛出异常是否有效?
Is Throwing Exception to forcefully coming out of recursion efficient?
以下为伪代码
function x(node){
if(node.val == 42)
return true; // What if I throw an exception here ?
val1 = node.left ? x(node.left) : false ;
val2 = node.right ? x(node.right): false;
return val1 || val2 ;
}
假设我正在尝试在树中找到 42。使用上面的代码,如果我 return 一个有效值,它将冒泡到整个递归链,然后最终 return.
我的假设是,如果我只是抛出异常而不是在第 3 行 return 为真,它实际上不会通过递归链向上冒泡并直接 return 到调用者。
抛出一个异常来打破整个链条是一个好习惯吗?因为让我们说在 line 4
,它确实在某个嵌套堆栈中找到了 42,并且它仍然会在 line 5
进行递归。如果我只是在 line 3
处抛出一个异常,我们就可以避免不必要的计算。
问题更多是编译器内部的问题,如果我只是抛出一个未处理的异常,它还会冒泡到递归堆栈(这使得这种方法毫无意义)还是直接return到父调用的程序计数器。
出于几个不同的原因,我不建议抛出异常来破坏递归链。
效率。一般来说,抛出异常比从函数中 returning 慢得多。具体来说,当抛出异常时,运行时需要用当前堆栈跟踪填充异常,需要查看调用哪个处理程序,需要搜索要执行的 finally
块或 try
- with-resources 语句,为垃圾收集目的跟踪对象引用等。这仍然需要遍历调用堆栈,就像 returning a value up the call chain 一样,而且几乎肯定不会有任何比更标准的方法更快。
可用性。如果您抛出异常以中止递归链,那么调用您的函数的任何人都需要编写对该异常做出反应的代码。这意味着他们需要有单独的分支,而不是像写 if (myCall()) {...}
这样的事情,一个用于调用 return 正常值的地方,一个用于它抛出的情况。如果他们忘记这样做,运行时的代码可能会由于异常而失败,而实际上它的行为确实符合预期。更糟糕的是,您实际上 反转 异常的正常使用。如果您的代码在 成功 时抛出异常,而在 失败 时 return 有一个值,那么阅读您的代码的人可能会去"huh?" 至少一次才能明白你的意思。
清晰度。异常应该用于非常特殊的情况!异常机制旨在在根本没有可行的方法 return 值的情况下报告信息。将异常系统重新用于处理常规控制流会使代码更难阅读和维护,并且违反了最少意外原则。
虽然您最初的问题主要是关于效率点,但我认为其他两点 - 可用性和清晰度 - 仍然强烈支持不以这种方式做事。
以下为伪代码
function x(node){
if(node.val == 42)
return true; // What if I throw an exception here ?
val1 = node.left ? x(node.left) : false ;
val2 = node.right ? x(node.right): false;
return val1 || val2 ;
}
假设我正在尝试在树中找到 42。使用上面的代码,如果我 return 一个有效值,它将冒泡到整个递归链,然后最终 return.
我的假设是,如果我只是抛出异常而不是在第 3 行 return 为真,它实际上不会通过递归链向上冒泡并直接 return 到调用者。
抛出一个异常来打破整个链条是一个好习惯吗?因为让我们说在 line 4
,它确实在某个嵌套堆栈中找到了 42,并且它仍然会在 line 5
进行递归。如果我只是在 line 3
处抛出一个异常,我们就可以避免不必要的计算。
问题更多是编译器内部的问题,如果我只是抛出一个未处理的异常,它还会冒泡到递归堆栈(这使得这种方法毫无意义)还是直接return到父调用的程序计数器。
出于几个不同的原因,我不建议抛出异常来破坏递归链。
效率。一般来说,抛出异常比从函数中 returning 慢得多。具体来说,当抛出异常时,运行时需要用当前堆栈跟踪填充异常,需要查看调用哪个处理程序,需要搜索要执行的
finally
块或try
- with-resources 语句,为垃圾收集目的跟踪对象引用等。这仍然需要遍历调用堆栈,就像 returning a value up the call chain 一样,而且几乎肯定不会有任何比更标准的方法更快。可用性。如果您抛出异常以中止递归链,那么调用您的函数的任何人都需要编写对该异常做出反应的代码。这意味着他们需要有单独的分支,而不是像写
if (myCall()) {...}
这样的事情,一个用于调用 return 正常值的地方,一个用于它抛出的情况。如果他们忘记这样做,运行时的代码可能会由于异常而失败,而实际上它的行为确实符合预期。更糟糕的是,您实际上 反转 异常的正常使用。如果您的代码在 成功 时抛出异常,而在 失败 时 return 有一个值,那么阅读您的代码的人可能会去"huh?" 至少一次才能明白你的意思。清晰度。异常应该用于非常特殊的情况!异常机制旨在在根本没有可行的方法 return 值的情况下报告信息。将异常系统重新用于处理常规控制流会使代码更难阅读和维护,并且违反了最少意外原则。
虽然您最初的问题主要是关于效率点,但我认为其他两点 - 可用性和清晰度 - 仍然强烈支持不以这种方式做事。