【问题标题】:Why should iteration be used instead of tail recursion?为什么要使用迭代而不是尾递归?
【发布时间】:2013-08-25 21:28:27
【问题描述】:

什么是设计味道,递归中的不良实践? 一旦我看到 resharper 提出改进建议,我很快就在谷歌上环顾四周。 在将尾递归重构为迭代并将其称为设计气味的过程中,看到了许多 cmets。

public static void DebugOutput2(Exception ex) {
    if (ex == null) {
        return;
    }
    Debug.WriteLine(ex.Message);
    if (ex.InnerException != null) {
        DebugOutput2(ex.InnerException);
    }
}

// WAS REFACTORED TO

public static void DebugOutput(Exception ex) {
    if (ex == null) {
        return;
    }
    while (true) {
        Debug.WriteLine(ex.Message);
        if (ex.InnerException != null) {
            ex = ex.InnerException;
            continue;
        }
        break;
    }
}

编辑:基于 C# 编译器处理注释。看起来它现在是递归的
目标 .net 4.5 。 C# 5.0

ILDASM 尾递归版本的输出:显示递归调用而不是迭代

.method public hidebysig static void  DebugOutput(class [mscorlib]System.Exception ex) cil managed
{
  // Code size       54 (0x36)
  .maxstack  2
  .locals init ([0] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldnull
  IL_0003:  ceq
  IL_0005:  ldc.i4.0
  IL_0006:  ceq
  IL_0008:  stloc.0
  IL_0009:  ldloc.0
  IL_000a:  brtrue.s   IL_000e
  IL_000c:  br.s       IL_0035
  IL_000e:  ldarg.0
  IL_000f:  callvirt   instance string [mscorlib]System.Exception::get_Message()
  IL_0014:  call       void [System]System.Diagnostics.Debug::WriteLine(string)
  IL_0019:  nop
  IL_001a:  ldarg.0
  IL_001b:  callvirt   instance class [mscorlib]System.Exception [mscorlib]System.Exception::get_InnerException()
  IL_0020:  ldnull
  IL_0021:  ceq
  IL_0023:  stloc.0
  IL_0024:  ldloc.0
  IL_0025:  brtrue.s   IL_0035
  IL_0027:  nop
  IL_0028:  ldarg.0
  IL_0029:  callvirt   instance class [mscorlib]System.Exception [mscorlib]System.Exception::get_InnerException()
  IL_002e:  call       void ca1.Program::DebugOutput(class [mscorlib]System.Exception)
  IL_0033:  nop
  IL_0034:  nop
  IL_0035:  ret

} // end of method Program::DebugOutput

【问题讨论】:

  • 请注意,虽然 CLR 支持尾调用,但 the C# compiler doesn't emit them(我上次检查过)。在任何情况下,你都不太可能用这个特定的构造来破坏堆栈,而且性能并不是真正的问题,因为你已经在处理异常了。
  • 我会拒绝 while 循环以支持更接近 while(ex != null) {Debug.WriteLine(ex.Message);ex = ex.InnerException} 的内容。
  • 从性能角度考虑使用尾递归会发生什么。我们需要创建一个堆栈帧,复制一些东西并调用然后调用该函数。根据递归,这可能会导致堆栈崩溃(尽管在此示例中可能不会)并且肯定会导致非递归迭代的性能更差
  • 我认为它在这里无关紧要,但总的来说迭代优于递归,因为它的性能要好得多。 ReSharper 可能只是建议尽可能使用迭代而不是递归,而不考虑是否确实存在性能问题。
  • 这样的建议太粗鲁了。有很多算法,递归是自然的解决方案,例如,任何树遍历代码都更容易用它编写。但是,是的,它在 32 位 .NET 程序中具有 cooties,x86 抖动不会优化尾调用。如果递归深度不受实际上限的限制或深度大于 O(log n),那么您的程序很可能会使用该网站的名称来字节。

标签: c# design-patterns


【解决方案1】:

recursion 为每个级别(您处理的每个对象)进行额外的(递归)函数调用,这也意味着堆栈分配。

通常迭代是better,因为它不会进行额外的调用/分配。

当然,如果有 许多 个对象要处理,我认为在您的示例中并非如此。所以对你来说这不是问题,我想目的是教你一个更好的实践。

【讨论】:

  • 我认为任何半体面的编译器都会优化掉尾递归调用
  • 我知道它已经说过了,但是使调用尾递归的全部意义在于编译器可以优化递归。
【解决方案2】:

因为人们错误地更关心微优化而不是清晰易读的代码。

【讨论】:

  • 我确实更喜欢易于阅读和维护的 DRY 递归解决方案,而不是节省几纳秒。请注意,凯文强调了一个非常优雅的迭代。与往常一样,您可以编写好的和坏的递归。但是在我看来,将递归称为难闻的气味似乎太过分了。特别是当 SOLID 设计结果适用时。所以现在+1。在接受答案之前将等待看看会发生什么。谢谢 ILMTitan
  • 您还可以在递归解决方案中删除对 InnerException 的冗余空检查。
猜你喜欢
  • 2013-09-14
  • 1970-01-01
  • 1970-01-01
  • 2011-04-10
  • 2011-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-15
相关资源
最近更新 更多