C#: 递归调用中的异常处理

8

我有一个递归方法调用。当抛出任何异常时,我希望能看到它发生在递归调用堆栈的哪个位置。我有一个字段,它保存了代表递归堆栈的“路径”。

现在,我想将路径信息添加到可能在递归调用中抛出的任何异常中。

void Recursive(int x)
{
  // maintain the recursion path information
  path.Push(x);

  try
  {
    // do some stuff and recursively call the method
    Recursive(x + 6);
  }
  catch(Exception ex)
  {
    if (ex is RecursionException)
    {
      // The exception is already wrapped
      throw;
    }
    // wrap the exception, this should be done only once.
    // save the path and original exception to the wrapper.
    throw new RecursionException(path.ToString(), ex);
  }
  finally
  {
    // maintain the recursion path information
    path.Pop()
  }
}

看起来太复杂了。并不只有一种方法。可能有二十甚至更多的地方需要编写这段代码。

有没有更简单的实现方法呢?


编辑:我想要一个简单得多的情况,其中没有这样递归调用该方法的开销,因为我有许多这样的递归调用,不仅有一个方法,还有几个方法互相递归调用,这已经足够复杂了。

所以我想避免整个 try-catch 块,但我找不到任何解决方案。

对于在我的自己的代码中抛出的异常,这不是一个大问题,因为它可能包括从开始到结束的路径。但是对于其他异常,这是一个问题。


编辑:这些异常需要在任何其他代码中被封装,而不仅仅是在调用递归方法时:

  try
  {
    int a = 78 / x; // DivisionByZeroExeption        

    Recursive(x + 6);

    this.NullReference.Add(x); // NullReferenceException
  }

所以只包装对Recusive的调用是不起作用的。

有许多这样的方法,具有不同的签名,执行不同的操作,唯一共同的是异常处理。


2
你的“路径”和异常中存在的调用堆栈属性有什么区别? - Mats Fredriksson
调用栈只包含方法名,这些方法名始终相同。在这种情况下,它将是“递归->递归->递归->递归”,这并没有说明任何内容。递归实际上是爬树,路径代表以人类可读的形式到达当前分支的路径。 - Stefan Steinegger
将所有在传递给Recursive的方法中完成的工作都包装在异常中,使用我的扩展方法处理不同的签名很棘手,尽管您可以使用泛型来指定参数类型,但是任意签名可能需要编写每个重载才能实现... - Dave D
5个回答

5

对异常处理稍作简化:

void Recursive(int x)
{
    // maintain the recursion path information
    path.Push(x);

    try
    {
        // do some stuff and recursively call the method
        Recursive(x + 6);
    }
    catch( RecursionException )
    {
        throw;
    }
    catch( Exception )
    {
        throw new RecursionException(path.ToString(), ex);
    }
    finally
    {
        // maintain the recursion path information
        path.Pop()
    }
}

如果有需要,您可以从异常中获取调用堆栈信息。除此之外,您可以将其编写为代码片段,并在需要重复使用时插入。

还有下面这种可能性,虽然速度较慢但可以正常工作:

void DoStuff()
{
    this.Recursive(1, this.RecursiveFunction1);
    this.Recursive(2, this.RecursiveFunction2);
}

bool RecursiveFunction1(int x)
{
    bool continueRecursing = false;

    // do some stuff
    return continueRecursing;
}

bool RecursiveFunction2(int y)
{
    bool continueRecursing = false;

    // do some other stuff here
    return continueRecursing;
}

private void Recursive(int x, Func<int, bool> actionPerformer)
{
    // maintain the recursion path information
    path.Push(x);

    try
    {
        // recursively call the method
        if( actionPerformer(x) )
        {
            Recursive(x + 6, actionPerformer);
        }
    }
    catch( RecursionException )
    {
        throw;
    }
    catch( Exception ex )
    {
        throw new RecursionException(path.ToString(), ex);
    }
    finally
    {
        // maintain the recursion path information
        path.Pop();
    }
}

当然可以,这样修复了愚蠢的 is 问题。但实际上,我想要避免整个try-catch的操作,因为在很多地方都需要维护它太痛苦了。 - Stefan Steinegger
我已添加了一个版本,通过传递一个带有int参数并返回bool的委托来完成工作。这样,您可以传递一个实际执行工作并返回是否继续递归的方法。此时,您的实际递归方法将只是一个用于执行重要工作的框架。但它会比以前的代码慢。 - Dave D
递归方法都有不同的签名。如果我必须拆分它们中的每一个,那只会让它变得更加复杂。 - Stefan Steinegger
它们是否都有不同的返回类型,还是总是能够返回是否继续递归?如果它们确实具有不同的返回类型,您是否能够将返回值移动到输出参数中? - Dave D
它们可以尽可能地不同...我实际上不想让调用它们变得更加复杂。递归调用结构已经足够复杂了。 - Stefan Steinegger
显示剩余2条评论

4
把捕获处理程序从递归函数中拿出来,只写递归而不处理会怎么样?
void StartRecursion(int x)
{
    try
    {
        path.Clear();
        Recursive(x);
    }
    catch (Exception ex)
    {
        throw new RecursionException(path.ToString(), ex);
    }
}

void Recursive(int x)
{
    path.Push(x);
    Recursive(x + 6);
    path.Pop();
}

void Main()
{
    StartRecursion(100);
}

但是你如何将“路径”的详细信息传递到异常中呢? - Ian Ringrose
我看不出来重点在哪里。StartRecursion 在哪里使用?path.Pop 在哪里? - Stefan Steinegger
好的,我们现在实际上正在使用Ian Ringrose的解决方案:在异常情况下保留路径。谢谢。 - Stefan Steinegger

4
您的问题在于异常处理。通常,将异常包装在自己的异常中是一个不好的想法,因为它给调用方代码带来了负担,需要处理您的异常。调用者怀疑他们可能通过调用您的代码引发“路径未找到”异常,不能将其调用包装在捕获IOException的try-catch中。他们必须捕获您的RecursionException,然后编写大量代码来询问它以确定它实际上是什么类型的异常。有时这种模式是有道理的,但我认为这不是其中之一。
事实上,在这里使用异常处理是完全没有必要的。以下是解决方案的一些期望方面:
- 调用者可以捕获任何类型的异常 - 在调试构建中,调用者可以确定递归函数在抛出异常时正在执行的操作。
好的,如果这些是设计目标,那么就实施它吧:
class C
{
  private Stack<int> path

#if DEBUG

    = new Stack<int>();

#else

    = null;

#endif

  public Stack<int> Path { get { return path; } }

  [Conditional("DEBUG")] private void Push(int x) { Path.Push(x); }
  [Conditional("DEBUG")] private void Pop() { Path.Pop(); }
  public int Recursive(int n)
  { 
    Push(n);
    int result = 1;
    if (n > 1)
    {
      result = n * Recursive(n-1);
      DoSomethingDangerous(n);
    }
    Pop();
    return result;
  }
}

现在,调用方可以处理它:

C c = new C();
try
{
  int x = c.Recursive(10);
}
catch(Exception ex)
{

#if DEBUG

  // do something with c.Path

您看到我们正在做什么吗?我们利用异常会停止递归算法的事实。我们最后想要做的是通过在 finally 中弹出来清理路径; 我们希望在异常情况下弹出操作被忽略!
有道理吗?

是的,这很有道理。Ian Ringrose已经建议过类似的东西,我接受了他的答案。顺便说一下,路径始终存在,我不仅需要它来处理异常,而且它也不是仅限于调试功能。但是对于解决方案来说,这并不重要。无论如何,谢谢! - Stefan Steinegger
使用条件属性是很不错的,但是我经常需要这种调试信息来解决客户问题,需要根据具体情况进行思考。 - Ian Ringrose

3

我认为你正在尝试将递归路径包含在异常详细信息中,以便帮助调试。

不妨尝试这个方法。

public void Recursive(int x)
{
  try
  {
    _Recursive(x)
  }
  catch
  { 
    throw new RecursionException(path.ToString(), ex);
    clear path, we know we are at the top at this point
  }
}

private void _Recursive(int x)
{
    // maintain the recursion path information
    path.Push(x);

    _Recursive(x + 6);

    //maintain the recursion path information
    //note this is not in a catch so will not be called if there is an exception
    path.Pop()
}

如果你正在使用线程等技术,你可能需要考虑将路径存储在线程本地存储中。


如果您不希望强制调用者处理RecursionException,您可以将“path”公开,以便调用者可以访问它。 (如Eric Lippert后来的回答所述)

或者,当捕获异常时,您可以将路径记录到错误日志系统中,然后重新引发异常。

public void Recursive(int x)
{
  try
  {
    _Recursive(x)
  }
  catch
  { 
    //Log the path to your loggin sysem of choose
    //Maybe log the exception if you are not logging at the top 
    //   of your applicatoin         
    //Clear path, we know we are at the top at this point
  }
}

这样做的好处是调用者完全不需要知道“路径”。

最终取决于您的调用者需要什么,我认为您是此代码的调用者,因此我们在这个层面上尝试猜测是没有意义的。


是的,这实际上会简化它。在出现异常的情况下,它只保留路径并仅在顶部处理它。我实际上必须删除所有异常处理程序和finally块才能使其工作,但删除代码总是很容易 :-) - Stefan Steinegger

0
void Recursive(int x)
{
  // maintain the recursion path information
  path.Push(x);

  try
  {
    // do some stuff and recursively call the method
    Recursive(x + 6);
  }
  finally
  {
    // maintain the recursion path information
    path.Pop()
  }
}

void Recursive2(int x)
{
  try
  {
     Recursive(x);
  }
  catch()
  {
      // Whatever
  }
}

这样你只需要处理一次,如果出现异常,Recursive2会处理它并中止递归。


但是你如何将“路径”的详细信息传递到异常中呢? - Ian Ringrose

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接