【问题标题】:Common programming mistakes in .Net when handling exceptions? [closed]处理异常时.Net中的常见编程错误? [关闭]
【发布时间】:2011-02-22 11:01:21
【问题描述】:

您在处理异常时遇到过哪些最常见的错误?

似乎异常处理可能是学习如何在 .Net 中“正确”做的最难的事情之一。特别是考虑到Common programming mistakes for .NET developers to avoid? 的当前排名第一的答案与异常处理有关。

希望通过列出一些最常见的错误,我们都可以学会更好地处理异常。

【问题讨论】:

标签: c# .net vb.net exception-handling


【解决方案1】:

您在处理异常时遇到过哪些最常见的错误?

我能想到很多。

首先阅读我关于将异常分类为烦恼愚蠢致命外生的文章: p>

http://ericlippert.com/2008/09/10/vexing-exceptions/

一些常见错误:

  • 无法处理外部异常。
  • 未能处理令人烦恼的异常。
  • 构造方法会引发令人烦恼的异常。
  • 处理您实际上无法处理的异常,例如致命异常。
  • 处理隐藏代码中错误的异常;不要处理愚蠢的异常,修复错误使其不会被抛出

  • 安全错误:进入不安全模式

    try
    {
      result = CheckPassword();
      if (result == BadPassword) throw BadPasswordException();
    }
    catch(BadPasswordException ex) { ReportError(ex); return; }
    catch(Exception ex) { LogException(ex); }
    AccessUserData();
    

    看看发生了什么?我们未能进入不安全模式。如果 CheckPassword 抛出 NetworkDriverIsAllMessedUpException,那么我们会捕获它、记录它并访问用户的数据,而不管密码是否正确。无法进入安全模式;当你遇到任何异常时,假设最坏的情况。

  • 安全错误:产生直接或间接泄露敏感信息的异常。

    这不完全是关于处理代码中的异常,而是关于产生由恶意代码处理的异常。

    有趣的故事。在 .NET 1.0 交付给客户之前,我们发现了一个错误,在该错误中可以调用引发异常的方法“调用此方法的程序集无权确定文件 C:\foo.txt 的名称”。伟大的。谢谢你让我知道。是什么阻止了所述程序集捕获异常并询问其消息以获取文件名?没有。我们在发货前解决了这个问题。

    这是一个直接的问题。一个间接的问题是我在LoadPicture 的VBScript 中实现的问题。根据不正确的参数是目录、不是图片的文件还是不存在的文件,它会给出不同的错误消息。这意味着您可以将其用作非常慢的磁盘浏览器!通过尝试一大堆不同的事情,您可以逐渐了解某人硬盘上的文件和目录。应该设计异常,以便如果它们由不可信的代码处理,该代码不会从他们为导致异常所做的任何事情中了解用户的私人信息。 (LoadPicture 现在提供的错误信息少了很多。)

  • 安全和资源管理错误:不清理资源的处理程序是资源泄漏等待发生。恶意的部分信任代码可以将资源泄漏用作拒绝服务攻击,这些代码会故意造成异常产生情况。

  • 鲁棒性错误:处理程序必须假定程序状态混乱,除非处理特定的外生异常。对于 finally 块尤其如此。当您处理意外的异常时,您的程序中完全有可能甚至很可能发生了严重的问题。您不知道您的任何子系统是否正常工作,如果它们正常工作,调用它们是否会使情况变得更好或更糟。如果可能,集中精力记录错误并保存用户数据,并尽可能干净地关闭。假设一切正常。

  • 安全错误:需要撤消具有安全影响的临时全局状态突变 任何可能具有恶意的代码可以运行之前。恶意代码可以运行 before finally 块运行!有关详细信息,请参阅我的文章:

http://blogs.msdn.com/ericlippert/archive/2004/09/01/224064.aspx

【讨论】:

  • 虽然只有在可以“处理”异常的情况下才应该在异常之后恢复,但在许多情况下(例如从用户提供的文件导入“文档”时),99% 的异常都可以通过丢弃任何部分构造的对象并报告操作失败来“正确处理”。在编写良好的应用程序中,尝试加载不是有效文件的内容不应使应用程序崩溃,而应简单地失败。一些正在构建的对象可能已经损坏,但如果它们在堆栈展开期间被丢弃,那应该不会伤害任何东西
  • 我希望像 C# 和 VB.NET 这样的 .NET 语言允许清理代码知道其关联的 try 的结果,而无需 catch 任何东西。如果偏离正常程序流程可能使对象处于无效状态,那么如果该对象可能是一个严重的问题,并且如果它要被丢弃则没有问题。与其说异常默认是不可恢复的,不如说堆栈展开应该明确地使潜在损坏的对象无效。这将允许在安全时恢复,但在必要时强制关闭。
  • @supercat:那太好了。 CLR 支持 try-fault 块,它类似于 finally 块,仅在出现异常时才执行,但它没有您描述的那种清理语义。我希望看到的是一个类似 STM 的系统,它可以在发生异常时回滚损坏对象的状态,但迄今为止的实验表明,性能成本太高了。
  • 回滚很好,但我喜欢的语义是“回滚或无效”。我更喜欢finally(Exception ex) 语法[并且有支持interface IDisposableEx {void Dispose(Exception ex);}using 鸭类型的东西] 因为如果代码尝试“正常”退出但没有@,这将允许事务块之类的东西抛出异常987654331@或Rollback,如果被保护块抛出但清理成功,则通过原始异常,或者如果被保护块抛出并且清理失败,则将原始异常包装在清理异常中。
【解决方案2】:

像这样重新抛出异常:

try 
{ 
   // some code here
}
catch(Exception ex)
{
   // logging, etc
   throw ex;
}

这会杀死堆栈跟踪,使可用性大大降低。正确的重新抛出方法是这样的:

try 
{ 
   // some code here
}
catch(Exception ex)
{
   // logging, etc
   throw;
}

【讨论】:

  • 他在问题中提到了这个..
  • @BlueRaja:我实际上并没有点击链接。呃,好吧。在这里提到它不会有什么坏处,因为它仍然与这个问题有关。
  • “正确的方法”有点极端; throw ex 有有效案例。
  • @Dour High Arch:比如?我不是说没有。就是想不出例子。
  • @Dour: throw ex; 在您的情况下无效。 throw new SaferException(...) 是你想要的。
【解决方案3】:

在许多情况下您应该尝试捕获特定异常时捕获所有异常:

try {
  // Do something.
} catch (Exception exc) {
  // Do something.
}

而不是:

try {
  // Do something.
} catch (IOException exc) {
  // Do something.
}

应按从最具体到最不具体的顺序排列例外情况。

【讨论】:

    【解决方案4】:

    用无意义的消息重新抛出异常。

    try
    {
        ...
    }
    catch (Exception ex)
    {
       throw new Exception("An error ocurred when saving database changes").
    }
    

    你不会相信我在生产环境中经常看到这样的代码。

    【讨论】:

    • 这个例子的另一个坏处是原始异常没有作为内部异常传递给新的异常对象。可能比无意义的错误消息更糟糕,至少内部异常会为原始问题提供线索。
    • 如果原始异常包含有关服务器实现细节的敏感信息,并且将处理异常的调用者不受信任,这通常是一个的主意。例如,试图对数据库进行 SQL 注入攻击的攻击者会喜欢查看详细的错误消息,准确描述查询是如何出错的。
    • 同意 Eric 的评论,所以我们在决定是否包含内部异常信息时应该考虑安全问题。但是,我看到的大部分代码实际上隐藏了异常的根本原因,这就是为什么我创建了另一个关于包含内部异常的答案。我会在那里发表关于它的评论。谢谢。
    【解决方案5】:

    没有人谈论看到像这样的空 catch 块......

     try{  
          //do something
        }
    catch(SQLException sqex){  
            // do nothing  
        }
    

    也永远不要使用异常处理来创建替代方法流...

     try{  
         //do something  
    
     }catch(SQLException sqex){  
    
         //do something else  
     }
    

    【讨论】:

    • +1 表示不使用控制流异常。
    【解决方案6】:

    不在IDisposable 对象上使用using

    File myFile = File.Open("some file");
    callSomeMethodWhichThrowsException(myFile);
    myFile.Close();
    

    myFile 在调用 myFile 的终结器之前不会关闭(可能永远不会),因为在调用 myFile.Close() 之前引发了异常。

    正确的做法是

    using(File myFile = File.Open("some file"))
    {
        callSomeMethodWhichThrowsException(myFile);
    }
    

    编译器会将其翻译成如下内容:

    File myFile = File.Open("some file");
    try
    {
        callSomeMethodWhichThrowsException(myFile);
    }
    finally
    {
        if(myFile != null)
            myFile.Dispose(); //Dispose() calls Close()
    }
    

    因此,即使遇到异常,文件也会关闭。

    【讨论】:

      【解决方案7】:

      重新抛出捕获的异常时忘记设置内部异常

      try
      {
          ...
      }
      catch (IOException ioException)
      {
          throw new AppSpecificException("It was not possible to save exportation file.")
          // instead of
          throw new AppSpecificException("It was not possible to save exportation file.", ioException);
      }
      

      当我发布此答案时,我忘记提及出于安全原因,我们应该始终考虑何时包含内部异常。正如Eric Lippertanother answer for this topic 上指出的那样,一些异常可以提供有关服务器实现细节的敏感信息。因此,如果将处理异常的调用者不受信任,则包含内部异常信息不是一个好主意。

      【讨论】:

      • +1 表示内部异常 :)
      • 没想到这一点。谢谢!
      • 这里有一个不好的例子——建议不要使用 ApplicationException
      • 我将示例从使用 ApplicaationException 更改为 AppSpecificException。感谢您指出这一点。
      【解决方案8】:

      空接:

      //What's the point?
      catch()
      {}
      

      重新抛出:

      //Exceptions are for *adding* detail up the stack
      catch (Exception ex)
      {throw ex;}
      

      【讨论】:

      • Empty catch 是不好的编程习惯?您会建议执行以下操作不是一个好主意?假设有一个程序在屏幕上显示天气信息。每五分钟,它连接到数据库以检索天气更新,并显示新的天气报告以及检索到的最后一次天气报告的时间。那么将数据库连接放在Try 和一个空的Catch 中是不合适的,这样如果程序无法获得新的天气报告,程序可以继续显示五分钟前的天气?
      • @Rising Star:在这种情况下,您应该记录一个数据库错误。
      • @Rising Star 绝对会很糟糕,catch 应该记录事件和/或让用户知道下一个报告被延迟
      • 空捕获在 99% 的情况下都是错误的。我见过一两个可以接受的例子。每条规则都有例外(双关语无意!)(嗯,几乎每条规则;规则有例外,每条规则都有例外......)
      • @Rising Star:在这种情况下,您不应使用空捕获。您应该捕获可能来自数据库操作的异常类型,仅此而已。您试图忽略数据库异常,而不是所有异常。例如,您不想忽略NullReferenceException
      【解决方案9】:

      假设涵盖许多场景的异常是特定的。一个真实的场景是一个 Web 应用程序,其中异常处理始终假定所有错误都是会话超时,并将所有错误记录并报告为会话超时。

      另一个例子:

      try
      {
           Insert(data);
      }
      catch (SqlException e)
      {
         //oh this is a duplicate row, lets change to update
         Update(data);
      }
      

      【讨论】:

        【解决方案10】:

        记录 Exception.Message 而不是 Exception.ToString()

        很多时候,我看到代码只记录异常消息,而它应该记录 ToString 方法的返回。 ToString 提供了比 Message 更多的异常信息。除了消息之外,它还包括内部异常和堆栈跟踪等信息。

        【讨论】:

          【解决方案11】:

          试图捕获 OutOfMemoryExceptionStackOverflowException - 它们会导致运行时关闭,因此可以从同一进程中(甚至从整个 CLR 中捕获它们?)

          OutOfMemoryException:当没有足够的内存继续执行程序时抛出的异常。

          "从 .NET Framework 2.0 版本开始,StackOverflowException 对象无法被 try-catch 块捕获,相应的进程默认终止。因此,建议用户编写代码来检测和防止堆栈溢出。”

          【讨论】:

            【解决方案12】:

            未能在 catch 处理程序中捕获可能的异常。这可能会导致错误的异常向上传播。

            例如:

            try
            {
                DoImportantWork();
            }
            catch
            {
                Cleanup();        
                throw;
            }
            

            如果Cleanup() 抛出异常会怎样?您不希望在此 catch 处理程序中看到指向 Cleanup() 方法的异常。您想要原始错误。您可以尝试记录清理错误,但即使是您的日志记录代码也需要异常处理以避免从中引发异常。

            try
            {
                DoImportantWork();
            }
            catch
            {
                try
                {
                    Cleanup();        
                }
                catch
                {
                    // We did our best to clean up, and even that failed.
                    // If you try to log this error, the logging may throw yet another Exception.
                }
                throw;
            }
            

            【讨论】:

            • -1 表示空的 catch 子句。
            • +1:好点。空渔获物本身并没有错……只要确保您了解后果即可。例如,如果不传播异常,您将如何实现 IDisposable.Dispose?有时您只需要下注,尤其是当您的日志记录组件发生故障时。
            • @Brian:没错。我什至见过 Debug.WriteLine 抛出异常!在某些时候,你只需要放弃。
            【解决方案13】:

            错误

            try
            {
               // Do something stupid
            }
            catch
            {
               // ignore the resulting error because I'm lazy, and later spend
               // a week trying to figure out why my app is crashing all over
               // the place.
            }
            

            更好

            try
            {
                /// do something silly.
            }
            catch (InvalidOperationException ex)
            {
                /// respond, or log it.
            }
            catch (Exception e)
            {
                /// log it.
            }
            

            【讨论】:

              【解决方案14】:

              使用异常进行正常的流量控制。例外应该例外。如果这是一个好的/预期的操作,请使用返回值等。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2020-01-04
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-09-07
                相关资源
                最近更新 更多