【问题标题】:How defensively should I program? [closed]我应该如何防守? [关闭]
【发布时间】:2009-06-27 17:10:40
【问题描述】:

我正在使用一个用于创建数据库连接的小例程:

之前

public DbConnection GetConnection(String connectionName)
{
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);
   DbConnection conn = factory.CreateConnection();
   conn.ConnectionString = cs.ConnectionString;
   conn.Open();

   return conn;
}

然后我开始查看 .NET 框架文档,了解各种事物的记录行为是什么,并看看我是否可以处理它们。

例如:

ConfigurationManager.ConnectionStrings...

documentation 表示如果无法检索集合,调用 ConnectionStrings 会引发 ConfigurationErrorException。在这种情况下,我无法处理这个异常,所以我会放手。


下一部分是 ConnectionStrings 的实际索引以查找 connectionName

...ConnectionStrings[connectionName];

在这种情况下,ConnectionStrings documentation 表示如果找不到连接名称,该属性将返回 null。我可以检查是否发生了这种情况,并抛出一个异常让某人高高在上,他们给出了一个无效的连接名称:

ConnectionStringSettings cs= 
      ConfigurationManager.ConnectionStrings[connectionName];
if (cs == null)
   throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

我重复同样的练习:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);

GetFactory 方法没有文档说明如果找不到指定 ProviderName 的工厂会发生什么情况。没有记录返回null,但我仍然可以防御,检查是否为空:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);
if (factory == null) 
   throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

接下来是 DbConnection 对象的构造:

DbConnection conn = factory.CreateConnection()

documentation 再次没有说明如果它无法创建连接会发生什么,但我可以再次检查 null 返回对象:

DbConnection conn = factory.CreateConnection()
if (conn == null) 
   throw new Exception.Create("Connection factory did not return a connection object");

接下来是设置 Connection 对象的属性:

conn.ConnectionString = cs.ConnectionString;

文档没有说明如果无法设置连接字符串会发生什么。它会抛出异常吗?它会忽略它吗?与大多数例外情况一样,如果在尝试设置连接的 ConnectionString 时出现错误,我无法从中恢复。所以我什么都不做。


最后,打开数据库连接:

conn.Open();

DbConnection 的Open method 是抽象的,因此由 DbConnection 的任何提供者来决定它们抛出什么异常。抽象的开放方法文档中也没有关于如果出现错误我会发生什么的指导。如果连接出现错误,我知道我无法处理它 - 我必须让它冒泡,调用者可以向用户显示一些 UI,然后让他们再试一次。


之后

public DbConnection GetConnection(String connectionName)
{
   //Get the connection string info from web.config
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];

   //documented to return null if it couldn't be found
    if (cs == null)
       throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);

   //Undefined behaviour if GetFactory couldn't find a provider.
   //Defensive test for null factory anyway
   if (factory == null)
      throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

   //Have the factory give us the right connection object
   DbConnection conn = factory.CreateConnection();

   //Undefined behaviour if CreateConnection failed
   //Defensive test for null connection anyway
   if (conn == null)
      throw new Exception("Could not obtain connection from factory");

   //Knowing the connection string, open the connection
   conn.ConnectionString = cs.ConnectionString;
   conn.Open()

   return conn;
}

总结

所以我的四行函数变成了 12 行,并且需要 5 分钟的文档查找。最后,我确实发现了一种允许方法返回 null 的情况。但实际上我所做的只是将访问冲突异常(如果我尝试在空引用上调用方法)转换为 InvalidArgumentException

我还发现了两种可能存在 null 返回对象的情况;但我又一次只用一个例外换了另一个。

从积极的方面来说,它确实发现了两个问题,并解释了异常消息中发生的事情,而不是在路上发生的坏事(即责任止步于此)

但这值得吗?这是矫枉过正吗?这种防御性编程是否出错了?

【问题讨论】:

  • 这很好。我唯一不同的是使用 String.Format 来构建消息而不是附加字符串。我还将消息放入资源中,以允许本地化。
  • 哪个例程更容易调试?哪个更容易维护?增加的复杂性是否足以让您受益?
  • 嗯,最容易编写的代码是完全没有错误检查的代码。而且没有任何错误检查的代码也更容易阅读。

标签: c# exception-handling defensive-programming


【解决方案1】:

手动检查配置并抛出异常并不比在缺少配置时让框架抛出异常更好。无论如何,您只是在重复发生在框架方法内部的前提条件检查,这会使您的代码变得冗长而没有任何好处。 (实际上,您可能会删除信息,方法是将所有内容都作为基本 Exception 类抛出。框架抛出的异常通常更具体。)

编辑:这个答案似乎有点争议,所以有点详细说明:防御性编程意味着“为意外做好准备”(或“偏执”),其中一种方法是进行大量的前置条件检查.在许多情况下,这是一种很好的做法,但与所有做法一样,都应权衡成本与收益。

例如,抛出“无法从工厂获取连接”异常并没有提供任何好处,因为它没有说明为什么无法获得提供程序 - 并且如果提供者为空,那么下一行无论如何都会引发异常。所以前提条件检查的成本(在开发时间和代码复杂性方面)是不合理的。

另一方面,验证连接字符串配置是否存在的检查可能是合理的,因为异常可以帮助告诉开发人员如何解决问题。无论如何,您将在下一行中获得的空异常不会告诉您丢失的连接字符串的名称,因此您的前置条件检查确实提供了一些价值。例如,如果您的代码是组件的一部分,则该值非常大,因为该组件的用户可能不知道该组件需要哪些配置。

防御性编程的另一种解释是,您不应该只检测错误情况,还应该尝试从可能发生的任何错误或异常中恢复。总的来说,我认为这不是一个好主意。

基本上你应该只处理你可以一些事情的异常。无论如何都无法恢复的异常应该向上传递给顶级处理程序。在 Web 应用程序中,顶级处理程序可能只显示一个通用错误页面。但在大多数情况下,如果数据库离线或缺少某些关键配置,则无需做太多事情。

这种防御性编程有意义的一些情况是,如果您接受用户输入,那么该输入可能会导致错误。例如,如果用户提供了一个 URL 作为输入,并且应用程序尝试从该 URL 获取某些内容,那么检查 URL 是否正确并处理可能由请求引起的任何异常是非常重要的。这使您可以向用户提供有价值的反馈。

【讨论】:

  • 在从 .NET 2.0 开始的 ASP.NET 应用程序中,ASP.NET 运行状况监控还将记录完整的异常,以及有关正在发生的事情的大量详细信息。
  • 我不同意。一段代码捕获一般异常并将 Message 更改为更具体的内容是有意义的;或检查先决条件。是的,他本可以允许 NullReferenceException 传播,但这不会给调用者一个线索,表明异常是由于无法构建连接。
  • 我不同意约翰的观点。调用者确实会知道导致异常的原因,因为异常包括堆栈跟踪。
  • 我同意 Jacob 的观点(保持链表继续运行)。明智的做法是拥有一个 AppDomain.UnexpectedException 处理程序,将 exception.InnerException 链和所有堆栈跟踪转储到某种日志文件,然后调用 Environment.FastFail。
  • 没错。异常的经验法则是,如果它由于配置或编码错误而被抛出,请不要捕获它。如果在用户输入过程中偶尔会遇到它,请继续并抓住它。上面显示的“防御性编码”示例确实是“编码恐怖”。
【解决方案2】:

嗯,这取决于你的观众是谁。

如果您编写的库代码希望被很多其他人使用,而他们不会与您讨论如何使用它,那么这并不过分。他们会感谢您的努力。

(也就是说,如果您这样做,我建议您定义比仅 System.Exception 更好的异常,以便那些想要捕获您的某些异常而不是其他异常的人更容易。)

但是,如果您只是自己(或您和您的伙伴)使用它,那么显然它是矫枉过正的,并且最终可能会通过降低您的代码的可读性来伤害您。

【讨论】:

    【解决方案3】:

    我希望我可以让我的团队编写这样的代码。大多数人甚至没有得到防御性编程的意义。他们所做的最好的事情就是将整个方法包装在一个 try catch 语句中,并让所有异常都由通用异常块处理!

    向你致敬,伊恩。我能理解你的困境。我自己也经历过同样的事情。但你所做的可能帮助一些开发人员数小时敲击键盘。

    请记住,当您使用 .net 框架 API 时,您对它有什么期望?什么看起来很自然?对您的代码执行相同操作。

    我知道这需要时间。但质量是有代价的。

    PS:您真的不必处理所有错误并抛出自定义异常。请记住,您的方法只会被其他开发人员使用。他们应该能够自己找出常见的框架异常。这不值得麻烦。

    【讨论】:

    • 好吧,至少在有异常的环境中,默认的失败状态是“停止”。如果您使用 C 之类的语言工作,而您的团队忽略了错误,那么您的应用将进入某种未定义状态并在未来某个未指定的时间点死掉。
    • 我同意伯纳德。我专门谈论 C#。
    • 我知道。那应该是“至少你是”,抱歉。
    • 我看不到如何需要所有额外的 try/catch 来避免“键盘敲击”。堆栈跟踪显示异常的来源,一旦你有了它,解决问题就很简单了。配置和编码错误不应该被捕获——让框架来做它的事。虽然我完全支持防御性编码,但它是上下文相关的,这里没有任何附加值。
    • 科里,我完全同意你的观点,我的观点就是这样。您不必处理所有异常,只需处理对当前方法有意义的异常。我的观点是,您不应该将所有方法包装在 try catch 中并让该方法吞下自己的异常,有些需要通过调用者。
    【解决方案4】:

    您的“之前”示例的特点是简洁明了。

    如果出现问题,框架最终会抛出异常。如果您对异常无能为力,您不妨让它向上传播调用堆栈。

    然而,有时在框架深处抛出异常并不能说明实际问题是什么。如果您的问题是您没有有效的连接字符串,但框架抛出了“无效使用 null”之类的异常,那么有时最好捕获异常并使用更有意义的消息重新抛出它。

    我确实经常检查空对象,因为我需要一个实际的对象来操作,如果对象为空,那么至少可以说,抛出的异常是倾斜的。但是如果我知道会发生什么,我只会检查空对象。一些对象工厂不返回空对象;它们会抛出异常,在这些情况下检查 null 将毫无用处。

    【讨论】:

    • 我最初在代码中有 cmets,从逻辑上描述发生了什么。我把它们拿出来是为了这个 SO 问题,让它看起来非常简洁,如果不加注释的话:)
    • 未注释的代码对于任何了解对象及其用途的人来说都是相当明显的。
    • cmets 的重点是帮助下一个人了解这些对象及其用途 - 下一个人很有可能就是我;从现在起8个月。第一行之前的注释说“静态 ConfigurationManager 类为我们从 web.config 中提取连接字符串信息”确实有助于解释发生了什么。 “哦,所以它们存储在 web.config 中?哦,是的,没错。”
    • 我不同意伊恩。不应期望程序员通过 cmets 记录框架——这就是框架文档的用途。注释应该说明why 你在做什么,而代码应该说明what 你在做什么。评论不应成为技术无能的创可贴。
    【解决方案5】:

    我不认为我会编写任何空引用检查逻辑 - 至少,不会按照您的方式编写。

    从应用程序配置文件中获取配置设置的程序在启动时会检查所有这些设置。我通常构建一个静态类来包含设置并在应用程序的其他地方引用该类的属性(而不是ConfigurationManager)。有两个原因。

    首先,如果应用程序配置不正确,它将无法运行。我宁愿在程序读取配置文件时知道这一点,也不愿在将来尝试创建数据库连接时知道这一点。

    其次,检查配置的有效性不应该是依赖配置的对象真正关心的问题。如果您已经预先执行了这些检查,那么让您自己在整个代码中到处插入检查是没有意义的。 (当然也有例外 - 例如,长时间运行的应用程序,您需要能够在程序运行时更改配置并将这些更改反映在程序的行为中;在这样的程序中,您需要需要设置时,请转到ConfigurationManager。)

    我也不会对 GetFactoryCreateConnection 调用进行空引用检查。您将如何编写测试用例来执行该代码?你不能,因为你不知道如何让这些方法返回 null - 你甚至不知道让这些方法返回 null 是可能。所以你已经替换了一个问题——你的程序可以在你不理解的条件下抛出NullReferenceException——另一个更重要的问题:在同样神秘的条件下,你的程序将运行你没有测试过的代码。

    【讨论】:

      【解决方案6】:

      我的经验法则是:

      如果 抛出的异常与 来电者。

      因此,NullReferenceException 没有相关消息,我会检查它是否为 null 并抛出带有更好消息的异常。 ConfigurationErrorException 是相关的,所以我没有抓住它。

      唯一的例外是 GetConnection 的“合同”不一定在配置文件中检索连接字符串。

      如果是这种情况,GetConnection 应该与自定义异常签订合同,说明无法检索连接,那么您可以将 ConfigurationErrorException 包装在自定义异常中。

      另一种解决方案是指定 GetConnection 不能抛出(但可以返回 null),然后在你的类中添加一个“ExceptionHandler”。

      【讨论】:

        【解决方案7】:

        您的方法文档丢失。 ;-)

        每个方法都有一些定义的输入和输出参数以及定义的结果行为。在您的情况下,例如:“在成功的情况下返回有效的打开连接,否则返回 null(或抛出 XXXException,如您所愿)。牢记此行为,您现在可以决定应该如何编程。

        • 如果您的方法应该公开失败的原因和原因的详细信息,请像您一样执行并检查并捕获所有内容并返回适当的信息。

        • 但是,如果您只是对打开的 DBConnection 感兴趣,或者只是对失败的 null(或某些用户定义的异常)感兴趣,那么只需将所有内容包装在 try/catch 中并返回 null(或某些异常)错误,否则对象。

        所以我想说,这取决于方法的行为和预期的输出。

        【讨论】:

        • 你会抛出异常而不返回异常,对吧?
        • 当然,应该总是抛出异常(还没有返回)
        【解决方案8】:

        一般而言,应捕获特定于数据库的异常并将其作为更一般的东西重新抛出,例如(假设的)DataAccessFailure 异常。大多数情况下,高级代码不需要知道您正在从数据库中读取数据。

        快速捕获这些错误的另一个原因是它们通常在消息中包含一些数据库详细信息,例如“没有这样的表:ACCOUNTS_BLOCKED”或“用户密钥无效:234234”。如果这传播给最终用户,则在几个方面是不好的:

        1. 令人困惑。
        2. 潜在的安全漏洞。
        3. 为您的公司形象丢脸(想象一下客户阅读语法粗鲁的错误消息)。

        【讨论】:

        • 您不应该从异常中删除有价值的信息!相反,您应该确保顶级处理程序不会向最终用户显示实际的异常和堆栈跟踪。
        • 我并不是说这应该被删除,但它必须被处理。而且您不想在顶级代码中捕获 SQLException。
        【解决方案9】:

        我会像您第一次尝试一样对其进行编码。

        但是,该函数的用户会使用 USING 块来保护连接对象。

        我真的不喜欢像您的其他版本那样翻译异常,因为它很难找出它崩溃的原因(数据库关闭?没有读取配置文件的权限等?)。

        【讨论】:

        • 好吧,如果您仔细观察,您会发现我实际上并没有吃/处理任何异常。最后,我只是防止了两个可能的空引用异常。
        • 好吧,你说得对,我没读好。我习惯了我自己的无法返回 NULL 的连接工厂。
        【解决方案10】:

        修改后的版本并没有增加太多价值,只要应用程序有一个 AppDomain.UnexpectedException 处理程序,它将exception.InnerException 链和所有堆栈跟踪转储到某种日志文件(或者甚至更好,捕获一个小型转储) ) 然后拨打Environment.FailFast

        根据这些信息,可以相当简单地查明问题所在,而无需通过额外的错误检查使方法代码复杂化。

        请注意,最好处理AppDomain.UnexpectedException 并调用Environment.FailFast,而不是使用顶级try/catch (Exception x),因为使用后者,问题的最初原因可能会被进一步的异常所掩盖。

        这是因为如果你捕捉到异常,任何打开的finally 块都会执行,并且可能会抛出更多异常,这将隐藏原始异常(或者更糟的是,它们会删除文件以尝试恢复某些状态,可能是错误的文件,甚至是重要的文件)。您永远不应该捕获表示您不知道如何处理的无效程序状态的异常 - 即使在顶级 main 函数 try/catch 块中也是如此。处理AppDomain.UnexpectedException 和调用Environment.FailFast 是一个不同的(更理想的)鱼缸,因为它会阻止finally 块运行,并且如果你试图停止你的程序并记录一些有用的信息而不造成进一步的损害,你肯定不想运行你的 finally 块。

        【讨论】:

          【解决方案11】:

          不要忘记检查 OutOfMemoryExceptions...您知道,它可能会发生。

          【讨论】:

          • 我希望这是讽刺!问题是,有人可能会看到并认真对待它。 (幸运的是,在 CLR 4.0 中,它们将无法捕获此类异常)。
          • 是的,这是讽刺,也许我需要澄清一下:)
          【解决方案12】:

          Iain 的变化在我看来是明智的。

          如果我正在使用一个系统并且使用不当,我希望获得有关滥用的最大信息。例如。如果我在调用方法之前忘记在配置中插入一些值,我想要一个 InvalidOperationException,其中包含一条详细说明我的错误的消息,而不是 KeyNotFoundException / NullReferenceException。

          这一切都与 IMO 上下文有关。在我的时代,我已经看到了一些相当难以理解的异常消息,但其他时候来自框架的默认异常非常好。

          一般来说,我认为最好谨慎行事,尤其是当您编写的内容被其他人大量使用或通常位于调用图中较难诊断的调用图中。

          我总是试图记住,作为一段代码或系统的开发人员,我比只使用它的人更能诊断故障。有时,几行检查代码 + 一条自定义异常消息可以累积节省数小时的调试时间(并且还可以让您自己的生活更轻松,因为您不会被拉到别人的机器上调试他们的问题)。

          【讨论】:

            【解决方案13】:

            在我看来,您的“后”样本并不是真正的防御性。因为防御性将检查您控制的部分,这将是参数connectionName(检查 null 或空,并抛出 ArgumentNullException)。

            【讨论】:

              【解决方案14】:

              在添加了所有防御性编程之后,为什么不拆分您拥有的方法?您有一堆不同的逻辑块,它们需要单独的方法。为什么?因为你封装了属于一起的逻辑,而你生成的公共方法只是以正确的方式连接这些块。

              类似这样的东西(在 SO 编辑器中编辑,因此没有语法/编译器检查。可能无法编译;-))

              private string GetConnectionString(String connectionName)
              {
              
                 //Get the connection string info from web.config
                 ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];
              
                 //documented to return null if it couldn't be found
                 if (cs == null)
                     throw new ArgumentException("Could not find connection string \""+connectionName+"\"");
                 return cs;
              }
              
              private DbProviderFactory GetFactory(String ProviderName)
              {
                 //Get the factory for the given provider (e.g. "System.Data.SqlClient")
                 DbProviderFactory factory = DbProviderFactories.GetFactory(ProviderName);
              
                 //Undefined behaviour if GetFactory couldn't find a provider.
                 //Defensive test for null factory anyway
                 if (factory == null)
                    throw new Exception("Could not obtain factory for provider \""+ProviderName+"\"");
                 return factory;
              }
              
              public DbConnection GetConnection(String connectionName)
              {
                 //Get the connection string info from web.config
                 ConnectionStringSettings cs = GetConnectionString(connectionName);
              
                 //Get the factory for the given provider (e.g. "System.Data.SqlClient")
                 DbProviderFactory factory = GetFactory(cs.ProviderName);
              
              
                 //Have the factory give us the right connection object
                 DbConnection conn = factory.CreateConnection();
              
                 //Undefined behaviour if CreateConnection failed
                 //Defensive test for null connection anyway
                 if (conn == null)
                    throw new Exception("Could not obtain connection from factory");
              
                 //Knowing the connection string, open the connection
                 conn.ConnectionString = cs.ConnectionString;
                 conn.Open()
              
                 return conn;
              }
              

              PS:这不是一个完整的重构,只做了前两个块。

              【讨论】:

              • 我更喜欢独立的逻辑。
              猜你喜欢
              • 1970-01-01
              • 2010-09-09
              • 2023-03-19
              • 2011-01-16
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-11-01
              相关资源
              最近更新 更多