【问题标题】:Where to implement try catch blocks?在哪里实现 try catch 块?
【发布时间】:2010-09-02 08:11:46
【问题描述】:

我一直在努力在哪里放置 try catch 块。例如,我有一个数据库类,其方法接受两个参数。 FindObject(字符串位置,字符串顺序)。此方法使用指定的 where 和 order 字符串执行 sql 查询。

在一个类中,我有一个名为 IsUsed 的属性,该属性如下所示:

public bool IsUsed 
{  
 get
 {   
  ClassA a = new ClassA();   
  Collection<ClassA> myCollection = a.FindObject("Id = 1","");

  if(..) // etc  
 } 
}

这个方法聪明不聪明没关系,我只想知道如果sql查询的执行出错了,try catch应该放在哪里。

我应该在哪里放置 try catch 以便通知用户出现问题?

  1. 在 FindObject 方法中?
  2. 在 IsUsed 属性中?
  3. IsUsed 属性在哪里调用?
  4. 其他地方?但是在哪里

【问题讨论】:

标签: c# error-handling


【解决方案1】:

我尝试遵循一个相当简单的规则:我设置一个 try..catch 块如果我能以合理的方式处理异常。如果对异常没有任何意义,我会让它冒泡到调用代码。

作为旁注,我会避免在属性 getter 中执行(可能很长)数据库调用。我通常尝试让属性设置或从支持字段中获取值,让其他方法执行数据库查找等。这将使编写调用代码的人更容易预测代码(属性访问通常是一种廉价的操作,而方法调用通常更昂贵)。

【讨论】:

  • 谢谢。我将 IsUsed 制作一个方法。我应该在 IsUsed 方法中引发一个消息框来通知用户发生了异常吗?
  • @Martijn:在不了解您的代码的情况下,我不能说显示消息框是否是一个好主意,但我尽量避免这样做,除非代码非常接近最终用户。如果代码在类库中,有效地显示消息框使其无法在 Web 应用程序或类似应用程序中使用。
  • 我还想补充一点,这可能是一个让异常被抛出的好方法,但有时你也必须使用 finally 块(尤其是在 DB 调用中,你要确保你关闭连接 - 如果不使用 using 块)。在这种情况下,您只需在 catch 部分重新抛出异常。
  • @Robert:好点。你不需要重新抛出,你可以简单地拥有一个 try..finally 块(没有 catch 块)。
  • 对于愚蠢的开发人员来说,很多事情可能看起来是错误的,但这并不意味着我们应该迎合他们。做正确的事情,如果这让糟糕的开发人员感到困惑,请在 cmets 中记录特殊形式。我不明白如何做错事以免愚蠢的开发人员感到困惑是一种编程的好方法。
【解决方案2】:

只有在你能做些什么的情况下才捕获异常。否则,在您的应用程序的“最高”级别捕获异常并执行处理它所需的任何操作,包括终止您的应用程序。

在具有 UI 的应用程序中,“最高”级别通常是事件处理程序,例如搜索按钮的单击处理程序。对于后台服务,“最高”级别通常是线程进程或计时器回调。

  UI application                          Background service

| System Code    |                      | System Code    |
+----------------+                      +----------------+
| Event Handler  | <- try/catch here -> | Thread proc    |
|                |                      |                |
| ...            |                      | ...            |
|                |                      |                |
| Method         | <- throw here     -> | Method         |

如果您让异常传播回系统代码,您将遇到未处理的异常并且您的应用程序将崩溃。

在 UI 应用程序中处理异常通常涉及显示消息框。有些异常不是致命的,操作可能会被重试(比如文件丢失或数据库查询失败),但其他异常是致命的,唯一的选择是终止应用程序。

后台服务将记录异常并可能重试该操作。如果多次重试失败,日志记录级别可能会提高以引起操作员的注意。

关于异常处理的良好做法:

  • 如果您捕获一个异常并重新抛出您自己的异常,则将原始异常包装为新异常的InnerException
  • 如果您捕获异常可能是为了进行一些清理,但由于您希望它冒泡而重新抛出它,那么请始终使用throw 重新抛出它而不指定任何异常。这将确保原始堆栈跟踪不会被破坏。
  • 在大多数情况下,您应该只在顶级处理程序中捕获基类 Exception
  • 使用finally 块或更好的IDisposable 模式在代码中执行适当的清理。
  • 将异常视为开发人员的“用户界面”,并相应地格式化异常消息。如果您需要为技术含量较低的用户提供更精美的用户界面,您可能应该隐藏技术含量较高的内容。
  • 尝试仅对异常情况(如意外错误)使用异常。不要为常见的错误情况抛出异常。检查字典中是否存在键不应引发异常,而是返回真/假值。

为了回答您的具体问题,我认为您不应在您的财产中发现任何异常。

【讨论】:

    【解决方案3】:

    你应该把 try-cacth 块放在你可以用捕获的异常做一些有意义的事情的地方。就像您记录它或向用户展示它一样。不要仅仅为了异常而捕获异常。

    【讨论】:

    • 但我的意思是,向用户展示它的最佳位置是什么?
    【解决方案4】:

    它应该是需要 try..catch 块的地方,并且可以处理它。我假设这将在 FindObject 方法中。

    因此,例如,在 FindObject 方法中,捕获并处理SqlException。如果需要将其移至更高级别以便更好地处理,则抛出异常,或者让它简单地冒泡。

    【讨论】:

    • 如果方法失败,我想通知用户发生了数据库错误。我应该如何以及在哪里执行此操作?
    • @Martijn - 最好的办法是将数据库代码从IsUsed 属性中移开,并有一个方法调用它。然后,您将能够通过某种初始化代码处理方法中的异常。对属性的数据库调用会很快变得丑陋。而是使用初始化代码并设置属性值。
    【解决方案5】:

    这很容易回答:您可以在哪里适当地处理它。

    也就是说,在给定的地方,你能根据捕获量做出有用的决定吗?能不能退点别的?你能告诉别人一些事情吗?您能否将错误转换为对应用程序其他层更有用的错误?

    如果答案是肯定的,那么你就在那里抓住它。如果所有这些都不是,那就不要抓住它,让另一个区域这样做。

    FWIW,恕我直言,您的 Get 实现也过于复杂。我认为通常情况下,人们不会期望房产做那种“工作”。不过,只是我的意见。

    【讨论】:

      【解决方案6】:

      我能想到的唯一规则是“在最低级别,您实际上可以对信息做一些有用的事情,并且您不会复制异常处理代码”。

      例如,如果您通常希望以相同的方式捕获所有与数据库访问相关的异常,我会创建一个数据抽象层(这样做有很多充分的理由),并将 try-catch 块放在那里。

      另一个极端是 Web 应用程序,您不希望出现任何此类异常,并且异常会被 Elmah 捕获。在这种情况下,您根本不想使用 try-catch 块,因为它会破坏日志记录。

      【讨论】:

        【解决方案7】:

        这取决于您要在哪里处理以继续处理。假设调用代码负责通知用户,那么一个好地方就是调用代码。调用代码可能需要在通知用户后询问用户其他内容。

        还要考虑重构抛出的异常。如果IsUsed 驻留在与数据库无关的类中并且FindObject 可以抛出SqlServerTimedOutException,它将弹出整个调用堆栈。如果此示意图完全令人困惑,请捕获异常并像这样重新抛出它:

             public bool IsUsed  
             {   
                 get 
                 {    
                  ClassA a = new ClassA();    
                  Collection<ClassA> myCollection;
        
                  try
                  {
                      myCollection = a.FindObject("Id = 1","");
                  }
                  catch (SqlServerTimedOutException ex)
                  {
                      throw new MyApplicationException("Cannot evaluate if is in use.", ex);
                  }
        
                  if(..) // etc   
                 } 
            }
        

        但不要过度使用它,因为它会使代码相当难看。慎重考虑。

        【讨论】:

          【解决方案8】:

          一般情况下,作为rule of thumb

          try 块包含可能导致异常的受保护代码

          在你的情况下,你可以像这样处理它:

          FindObject()
          {
          try{}catch{throw;//or throw new Exception("Some info");
          }
          
          IsUser
          {
          try{...a.FindObject("Id = 1",""); return true;}
          catch(Exception ex){Log(ex); return false;}
          }
          

          --编辑--

          这是对@controlbreak 评论的回应:

          MSDN 说:

          您可以使用 throw 语句显式抛出异常。您还可以使用 throw 语句再次抛出捕获的异常。 良好的编码习惯向异常添加信息,以便在调试时重新抛出以提供更多信息。

          【讨论】:

          • 为什么要捕获一个异常,什么都不做,然后再次抛出?
          • catch{throw;} 没用。无论如何,值得一提的是 catch{ 单独(不是 catch(Exception ...) 可以用来捕获非托管异常。
          • @Martinjn:你捕获了异常,然后再次抛出它,因为你目前没有处理程序,你不知道你想对特定错误做什么,但是你冒泡了通过再次抛出它,以便“上层”的人可能知道如何处理错误。例如:您可能会捕获“服务器忙”异常并且您可以通过尝试再次连接来处理它,但是您也捕获了一个异常说“服务器关闭”,现在您不知道该怎么做......所以您冒泡它了。可能调用方法可以优雅地捕获和处理错误。
          • @ace @Martijn 的意思是异常会自动抛出到顶层。这就是为什么没有必要抓住它们再扔掉它们。
          • @controlbreak:您能告诉我们,当您无法以合理的方式处理异常时,您会怎么做?如果对异常没有任何意义,您可以做些什么?请参阅 Fredrik Mörk 的回复 stackoverflow.com/questions/3624914/…
          【解决方案9】:

          就我而言,我放了 try catch 块:

          • 当需要 finally 来清理资源时
          • 在失败的情况下需要特定的操作流程时(用户重试、记录...)

          通常,我让异常冒泡到顶层,通常是一些 winform 控件事件处理程序。我习惯将try catch放在这里,使用应用程序级别的异常处理方法。

          否则,当我特别懒惰时,我会将 Application.ThreadException 事件挂钩到 static void Main() 中的 MessageBox.Show > 入口点方法。

          【讨论】:

          • 但是你不需要一个 catch 块来拥有一个 finally 块。
          【解决方案10】:

          尽早发现错误,并设计 API 调用,使其能够在不使用异常的情况下处理故障。未捕获的异常是对稳定性的威胁,五层以上您可能不知道是否在边缘情况下抛出异常,除非较低的 API 表示它们可能会失败。

          您通常可以将 api 设计为在其中包含一个结果对象,其中包含执行状态,这将告诉消费开发人员此方法可能会失败以及它是如何失败的。

          这种方法通常会给您提供有意义的方式来处理较低 api 部分中的异常,而不是让它们向上冒泡到调用代码。

          这就是说有些语言你可以声明一个方法会抛出异常以及它会抛出哪些异常,但是你编写的 C# 没有这个功能。

          【讨论】:

            【解决方案11】:

            看,如果您遇到任何错误,请将 IsUsed 设置为默认值。像这样的:

            public bool IsUsed 
            {  
             get
             {   
               try{
                 ClassA a = new ClassA();   
                 Collection<ClassA> myCollection = a.FindObject("Id = 1","");
            
                 if(..) // etc  
               } 
               catch { return false;}
             } 
            }
            

            因此,只要 FindObject 有任何错误,您就会产生 false。

            【讨论】:

            • 我想过这种方法,但我不喜欢,因为这种方法我永远不知道是否发生了数据库异常。所以这使得调试变得更加困难..
            • @Martijn:是的。切勿吞下异常,除非它确实是虚假的或者是不需要成功的操作的一部分,例如调试日志记录(对于某些程序)。
            • @Martijn 是的,当然,如果需要,您可以抛出异常。但通常如果属性中发生异常,我们只需从属性中返回默认值。如果您的异常很敏感,那么您可以记录异常消息并向用户显示一条好消息,而不是停止整个程序。
            【解决方案12】:

            我不同意你们中的大多数人。

            首先,我想推荐 Joel 的这篇好文章:http://www.joelonsoftware.com/items/2003/10/13.html

            记住这一点,我只使用 try-catch 块,尽可能靠近它们可能出错的地方。
            显然 UI 想听听它。让它有一个错误值,但不要隐藏实际问题的原因(通过捕获其他意外错误)。

            如果我正确理解您的代码,我想我会在实际查询中使用 try-catch,并将 IsUsed 设为 nullabe 以确保我理解它没有被赋值。

            【讨论】:

            • 理论上听起来不错,但实际情况是,您可能无法在抛出异常时处理它。我的意思是,也许你可以,在这种情况下,一定要抓住并处理。但是要求每个级别“处理”每个错误不仅会创建丑陋的代码,而且可能会创建不正确的代码。原因:产生异常的代码实际上可能没有足够的知识来处理它,只有更高的层才会有这些信息(例如,决定是否重试)。
            猜你喜欢
            • 2022-01-13
            • 2016-07-02
            • 2010-10-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-10-19
            • 2011-04-17
            • 2021-01-27
            相关资源
            最近更新 更多