catch语句中的附加try语句 - 代码味道?

时间:2009-12-08 16:31:32

标签: c# design-patterns try-catch

情况:

我的应用程序需要处理业务规则的第一步(初始的try-catch语句)。如果进程在该步骤中调用辅助方法时发生某个错误,我需要切换到catch语句中的第二个进程。备份过程使用相同的帮助程序方法。如果在第二个进程中发生同样的错误,我需要停止整个进程并抛出异常。

实现:

我打算在第一个try-catch语句的catch语句中插入另一个try-catch语句。

//run initial process
try
{
    //initial information used in helper method
    string s1 = "value 1";

    //call helper method
    HelperMethod(s1);
}
catch(Exception e1)
{
    //backup information if first process generates an exception in the helper method
    string s2 = "value 2";

    //try catch statement for second process.
    try
    {
        HelperMethod(s2);
    }
    catch(Exception e2)
    {
        throw e2;
    }
}

在此实现中避免代码味道的正确设计模式是什么?

我引起了一些混淆,并且忽略了当第一个进程失败并切换到第二个进程时,它会向辅助方法发送不同的信息。我已更新方案以反映整个过程。

15 个答案:

答案 0 :(得分:12)

如果HelperMethod需要第二次尝试,那么这没有什么直接的错误,但是catch中的代码试图做得太多,并且它会从e2中破坏堆栈跟踪。

你只需要:

try
{
    //call helper method
    HelperMethod();
}    
catch(Exception e1)
{
    // maybe log e1, it is getting lost here
    HelperMethod();
}

答案 1 :(得分:4)

我不会说它是,虽然我几乎肯定会将第二个代码块重构为第二个方法,所以要保持它的可理解性。并且可能会捕获比Exception更具体的内容。第二次尝试有时是必要的,特别是对于可能会抛出的Dispose()实现(WCF,我正在看着你)。

答案 2 :(得分:2)

将try-catch放在父级try-catch的catch中的一般想法对我来说似乎不是代码味道。我可以想到这样做的其他合理原因 - 例如,在清理失败的操作时,您不希望再抛出另一个错误(例如清理操作也失败)。但是,你的实现为我提出了两个问题:1)Wim的评论,2)你真的想完全忽视操作最初失败的原因(e1例外)吗?无论第二个进程成功还是失败,您的代码都不会对原始异常执行任何操作。

答案 3 :(得分:1)

一般来说,这不是问题,也不是我所知道的代码味道。

话虽如此,您可能希望在第一个辅助方法中处理错误而不是仅仅抛出它(因此,在那里处理对第二个辅助方法的调用)。只有这样才有意义,但这是一种可能的改变。

答案 4 :(得分:1)

是的,更通用的模式是基本方法包括接受int attempt参数的重载,然后有条件地递归调用自身。

   private void MyMethod (parameterList)
   {  MyMethod(ParameterList, 0)l }

   private void MyMethod(ParameterList, int attempt)
   {
      try { HelperMethod(); }
      catch(SomeSpecificException)
      {
          if (attempt < MAXATTEMPTS)
              MyMethod(ParameterList, ++attempt);
          else throw;
      }
   }

答案 5 :(得分:0)

不应该那么糟糕。只是清楚地记录你为什么要这样做,并且最明确地尝试捕获更具体的异常类型。

答案 6 :(得分:0)

如果你需要一些重试机制,你可能想要探索不同的技术,循环延迟等。

答案 7 :(得分:0)

如果你在catch中调用了一个不同的函数会更清楚一点,这样读者就不会认为你只是重新尝试相同的函数。如果您的示例中没有显示正在发生的状态,则应至少仔细记录。

你也不应该throw e2;这样:你应该只是throw;,如果你要处理你所捕获的例外情况。如果没有,你不应该尝试/捕获。

如果您没有引用e1,则应该只是catch (Exception)或更好catch (YourSpecificException)

答案 8 :(得分:0)

这是另一种模式:

// set up state for first attempt
if(!HelperMethod(false)) {
        // set up state for second attempt
        HelperMethod(true);
        // no need to try catch since you're just throwing anyway
}

此处HelperMethod

bool HelperMethod(bool throwOnFailure)

并且返回值指示是否成功发生(即,false表示失败并且true表示成功)。你也可以这样做:

// could wrap in try/catch
HelperMethod(2, stateChanger);

其中HelperMethod

void HelperMethod(int numberOfTries, StateChanger[] stateChanger)

其中numberOfTries表示在抛出异常之前尝试的次数,StateChanger[]是一个委托数组,它将在调用之间为您更改状态(即stateChanger[0]被调用在第一次尝试之前,在第二次尝试之前调用stateChanger[1],等等。)

最后一个选项表明您可能有一个有臭味的设置。看起来封装此过程的类负责跟踪状态(查找哪个员工)以及查找员工(HelperMethod)。通过SRP,这些应该是分开的。

当然,你需要catch一个比你现在更具体的例外(不要抓住基类Exception!),你应该throw而不是throw e {1}}如果需要在记录,清理等之后重新抛出异常

答案 9 :(得分:0)

如果您这样做是为了尝试从某种瞬态错误中恢复,那么您需要注意如何实现它。

例如,在您使用SQL Server镜像的环境中,您连接的服务器可能会停止成为主中间连接。

在这种情况下,您的应用程序尝试重新连接并重新执行新主服务器上的任何语句可能是有效的 - 而不是立即将错误发送回调用方。

您需要小心确保您调用的方法没有自己的自动重试机制,并且您的调用者知道您的方法中内置了自动重试。如果无法确保这种情况,可能会导致导致大量重试尝试的情况,从而导致共享资源(例如数据库服务器)过载。

您还应该确保捕获特定于您尝试重试的瞬态错误的异常。因此,在我给出的示例中,SqlException,然后检查错误是否SQL连接失败,因为主机不再是主服务器。

如果您需要多次重试,请考虑进行“自动退避”重试延迟 - 第一次失败会立即重试,第二次失败后延迟(比方说)1秒,然后加倍到最大值(比如说) )90秒。这应该有助于防止资源过载。

我还建议重构你的方法,这样你就没有内部的try / catch。

例如:

bool helper_success = false; 
bool automatic_retry = false; 
//run initial process
try
{
    //call helper method
    HelperMethod();
    helper_success = true; 
}
catch(Exception e)
{
    // check if e is a transient exception. If so, set automatic_retry = true 
} 


if (automatic_retry)
{    //try catch statement for second process.
    try
    {
        HelperMethod();
    }
    catch(Exception e)
    {
        throw;
    }
}

答案 10 :(得分:0)

您可以模拟C#的TryParse方法签名:

class Program
{
    static void Main(string[] args)
    {
        Exception ex;
        Console.WriteLine("trying 'ex'");
        if (TryHelper("ex", out ex))
        {
            Console.WriteLine("'ex' worked");
        }
        else
        {
            Console.WriteLine("'ex' failed: " + ex.Message);
            Console.WriteLine("trying 'test'");
            if (TryHelper("test", out ex))
            {
                Console.WriteLine("'test' worked");
            }
            else
            {
                Console.WriteLine("'test' failed: " + ex.Message);
                throw ex;
            }
        }
    }

    private static bool TryHelper(string s, out Exception result)
    {
        try
        {
            HelperMethod(s);
            result = null;
            return true;
        }
        catch (Exception ex)
        {
            // log here to preserve stack trace
            result = ex;
            return false;
        }
    }

    private static void HelperMethod(string s)
    {
        if (s.Equals("ex"))
        {
            throw new Exception("s can be anything except 'ex'");
        }
    }

}

答案 11 :(得分:0)

另一种方法是展平try / catch块,如果您使用的是异常快乐的API,则非常有用:

public void Foo()
{
  try
  {
    HelperMethod("value 1");
    return; // finished
  }
  catch (Exception e)
  {
     // possibly log exception
  }

  try
  {
    HelperMethod("value 2");
    return; // finished
  }
  catch (Exception e)
  {
     // possibly log exception
  }

  // ... more here if needed
}

答案 12 :(得分:0)

重试的选项(大多数人可能会火焰)将使用 goto 。 C#没有过滤异常,但可以类似的方式使用。

const int MAX_RETRY = 3;

public static void DoWork()
{
    //Do Something
}

public static void DoWorkWithRetry()
{
    var @try = 0;
retry:
    try
    {
        DoWork();
    }
    catch (Exception)
    {
        @try++;
        if (@try < MAX_RETRY)
            goto retry;
        throw;
    }
}

答案 13 :(得分:0)

在这种情况下你知道这个“异常”可能会发生,所以我更喜欢一个简单的方法,为未知事件留下例外。

//run initial process
try
{
    //initial information used in helper method
    string s1 = "value 1";

    //call helper method
    if(!HelperMethod(s1))
    {
        //backup information if first process generates an exception in the helper method
        string s2 = "value 2";
        if(!HelperMethod(s2))
        {
          return ErrorOfSomeKind;
        }
    }
    return Ok;
}
catch(ApplicationException ex)
{
    throw;
}

答案 14 :(得分:0)

我知道我最近完成了上面嵌套的try catch来处理解码数据,其中两个第三方库在解码失败时抛出异常(尝试json解码,然后尝试base64解码),但我的首选是拥有函数< strong> 返回一个可以检查的值。

我通常只使用抛出异常来提前退出,并通知链上有关错误的内容,如果它对进程致命的话。

如果函数无法提供有意义的响应,那通常不是致命问题(与错误的输入数据不同)。

似乎嵌套try catch的主要风险是你最终还会捕获可能发生的所有其他(可能很重要的)异常。