【问题标题】:How to avoid a stack overflow?如何避免堆栈溢出?
【发布时间】:2010-11-30 01:00:49
【问题描述】:

我使用 CSharpCodeProvider 编译我的代码,并在结果程序集中动态创建某个类的实例。比我称之为某种方法。如果该方法具有递归,我会得到 StackOverflowException 并且我的应用程序终止。

如何避免这种情况?

using System;
using System.Runtime.Remoting;
namespace TestStackOverflow
{
    class Program
    {
        class StackOver : MarshalByRefObject
        {
            public void Run()
            {
                Run();
            }
        }

        static void Main(string[] args)
        {
        AppDomain domain = AppDomain.CreateDomain("new");

        ObjectHandle handle = domain.CreateInstance(typeof (StackOver).Assembly.FullName, typeof (StackOver).FullName);
        if (handle != null)
        {
            StackOver stack = (StackOver) handle.Unwrap();
            stack.Run();
        }

    }
}
}

相关:

What is a stack overflow?

【问题讨论】:

  • 不要创建无限递归的循环。
  • 我认为没有人可以避免 StackOverflow
  • 你能发布一些示例代码吗?特别是具有递归的位。
  • 不要写任何代码。
  • 你调用Run(),它又调用Run(),然后调用Run(),然后Run()再次调用Run()。毕竟,Run() 被调用了几百次,导致......溢出堆栈!

标签: c# .net .net-2.0


【解决方案1】:

StackOverflow 表明您的递归太深并且堆栈内存不足。例如:

public class StackOver
{
    public void Run() 
    { 
        Run(); 
    }
}

这将导致堆栈溢出,因为 StackOver::Run() 将被一遍又一遍地调用,直到没有剩余内存为止。

我怀疑在您的情况下,您可能缺少终止条件,或者您正在运行太多递归迭代。

如果您想保持应用程序运行,请尝试:

namespace TestStackOverflow
{
    class Program
    {
        class StackOver : MarshalByRefObject
        {
            public bool Run()
            {
                return true; // Keep the application running. (Return false to quit)
            }
        }

        static void Main(string[] args)
        {
            // Other code...

            while (stack.Run());
        }

    }
}

【讨论】:

    【解决方案2】:

    如果递归导致堆栈溢出,则问题与编译类无关——递归函数需要终止条件,因为C# doesn't (usually) optimize tail calls

    【讨论】:

      【解决方案3】:

      我没有 CSharpCodeProvider 的良好背景,但我知道递归实现的算法可以用循环来实现

      【讨论】:

        【解决方案4】:

        使用递归函数避免堆栈溢出的唯一方法是有一个明确的退出条件,无论输入如何,最终都会满足。要么定义最大深度并在达到它后停止递归调用,要么确保检查的数据是有限的(并且在合理的范围内),或者两者兼而有之。

        【讨论】:

          【解决方案5】:

          好的。是否使用 CSharpCodeProvider 并不重要。我正在另一个域中使用反射加载程序集。我认为域是出于安全原因而创建的。我怎样才能保护应用程序免于终止???

          using System;
          using System.Runtime.Remoting;
          namespace TestStackOverflow
          {
              class Program
              {
                  class StackOver : MarshalByRefObject
                  {
                      public void Run()
                      {
                          Run();
                      }
                  }
          
                  static void Main(string[] args)
                  {
                  AppDomain domain = AppDomain.CreateDomain("new");
          
                  ObjectHandle handle = domain.CreateInstance(typeof (StackOver).Assembly.FullName, typeof (StackOver).FullName);
                  if (handle != null)
                  {
                      StackOver stack = (StackOver) handle.Unwrap();
                      stack.Run();
                  }
          
              }
          }
          }
          

          【讨论】:

          • Run 正在调用 Run。这就是无限递归。
          • 是的。我看到了这个。 .NET 框架中的域是出于安全原因而设计的。据我了解,我不能加载我的程序集,而是创建某个类的实例和调用方法。我以为我可以为另一个域中的代码设置权限。
          • 至强 - 你在拖钓吗?这属于 DailyWTF 吗?
          • 不,我不是在拖钓。我知道 StackOverflow 无法被捕获,但我认为 AppDomains 可以提供帮助.....
          【解决方案6】:

          Run 正在调用 Run。那就是无限递归。

              class StackOver : MarshalByRefObject
              {
                  public void Run()
                  {
                      Run(); // Recursive call with no termination
                  }
              }
          

          【讨论】:

            【解决方案7】:

            每次从方法 bar 调用方法 foo 时,都会将 bar 添加到调用堆栈中。调用堆栈用于跟踪代码在调用方法之前的位置,以便在 foo 完成时返回那里。

            以下递归函数

            int Factorial(int n) {
                if (n == 0) { return 1; }
                return n * Factorial(n - 1);
            }
            

            在调用 Factorial(5) 的几次递归之后,调用堆栈将如下所示:

            Factorial(5) -> Factorial(4) -> Factorial(3) -> Factorial(2) -> Factorial(1)
            

            此时 n 为 1,因此函数停止调用递归情况,而是返回 1。然后程序开始回卷调用堆栈,整个过程返回 120。

            如果没有调用堆栈,程序在执行完方法后将不知道返回到哪里。

            现在假设基本情况不存在,它看起来像这样:

            int Factorial(int n) {
                return n * Factorial(n - 1);
            }
            

            在多次调用 Factorial(5) 之后,调用堆栈将如下所示:

            Factorial(5) -> Factorial(4) -> Factorial(3) -> Factorial(2) -> Factorial(1) -> Factorial(0) -> Factorial(-1) -> Factorial(-2) -> Factorial(-3) -> Factorial(-4) -> Factorial(-5) -> Factorial(-6) -> Factorial(-7) -> Factorial(-8) -> Factorial(-9) -> Factorial(-10) -> Factorial(-11) -> Factorial(-12) -> Factorial(-13) -> Factorial(-14) -> Factorial(-15) etc…
            

            因为代码在任何时候都不会停止调用自己,它将永远继续下去,并且调用堆栈会不断增长,并且会占用越来越多的内存,直到超过已分配的内存和 StackOverflow 异常被抛出。

            有两种方法可以阻止这种情况发生,最好的方法取决于具体情况。

            1 提供基本情况。确保最终达到某种条件,从而阻止函数调用自身。在阶乘情况下,n == 1,但可能是已经过去了一定时间,它已经递归了一定次数,某些计算的某些结果在某些范围内,无论如何。只要它在堆栈太大之前停止回收。

            2 删除递归并重新编写它。任何递归算法都可以重写为非递归算法。它可能没有那么干净和优雅,但可以做到。在阶乘参数中,它可能类似于:

            int Factorial(int n) {
                int result = 1;
            
                for (int i = 0; i < n; i += 1) {
                    result *= n;
                }
            
                return result;
            }
            

            如果目标是一次又一次地连续运行相同的函数,那么你可以重写递归

            void Foo() {
                // Some code
                Foo();
            }
            

            作为

            void Foo() {
                while (true) { // Some code }
            }
            

            【讨论】:

              猜你喜欢
              • 2020-03-08
              • 2011-09-21
              • 2014-04-13
              • 1970-01-01
              • 2011-11-23
              • 2010-12-04
              • 2016-07-12
              • 2020-02-12
              • 2015-11-14
              相关资源
              最近更新 更多