我应该如何防守?

Ian*_*oyd 38 c# defensive-programming exception-handling

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

之前

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;
}
Run Code Online (Sandbox Code Playgroud)

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

例如:

ConfigurationManager.ConnectionStrings...
Run Code Online (Sandbox Code Playgroud)

文件说,打电话的ConnectionStrings抛出一个ConfigurationErrorException如果无法检索集合.在这种情况下,我无法处理此异常,所以我会放手.


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

...ConnectionStrings[connectionName];
Run Code Online (Sandbox Code Playgroud)

在这种情况下,ConnectionStrings文档说如果找不到连接名,该属性将返回null.我可以检查是否发生了这种情况,并抛出一个例外,让某人高兴他们给了一个无效的connectionName:

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

我重复同样的练习:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);
Run Code Online (Sandbox Code Playgroud)

GetFactory方法对如果指定一家工厂发生了什么没有文档ProviderName找不到.它没有记录返回null,但我仍然可以防御,并检查 null:

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

接下来是DbConnection对象的构造:

DbConnection conn = factory.CreateConnection()
Run Code Online (Sandbox Code Playgroud)

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

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

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

conn.ConnectionString = cs.ConnectionString;
Run Code Online (Sandbox Code Playgroud)

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


最后,打开数据库连接:

conn.Open();
Run Code Online (Sandbox Code Playgroud)

DbConnection 的Open方法是抽象的,因此它取决于从DbConnection下降的任何提供者来决定它们抛出的异常.抽象的Open方法文档中也没有关于如果出现错误我可能会发生什么的指导.如果连接有错误,我知道我无法处理它 - 我必须让它冒泡,调用者可以向用户显示一些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;
}
Run Code Online (Sandbox Code Playgroud)

摘要

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

我还捕获了两种可能存在null返回对象的情况; 但我再次只为另一个交易了一个例外.

从积极的方面来说,它确实遇到了两个问题,并解释了异常消息中发生的事情,而不是发生在路上的坏事(即降压停在这里)

但是这值得吗?这有点矫枉过正吗?这防御性节目是否出错了?

Jac*_*esB 31

手动检查配置并抛出异常并不比让框架在缺少配置时抛出异常更好.你只是复制了框架方法中发生的前置条件检查,它使你的代码冗长而没有任何好处.(实际上,您可能通过将所有内容作为基本Exception类来删除信息.框架抛出的异常通常更具体.)

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

例如,它没有提供任何好处来抛出"无法从工厂获得连接"异常,因为它没有说明为什么无法获取提供者 - 并且如果是,下一行将抛出异常provider为null.因此,前置条件检查的成本(开发时间和代码复杂性)是不合理的.

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

对防御性编程的不同解释是,您不仅应该检测错误条件,还应该尝试从可能发生的任何错误或异常中恢复.我不相信这是一个好主意.

基本上你应该只处理你可以做些什么的异常.无论如何都无法恢复的异常应该只是向上传递给顶级处理程序.在Web应用程序中,顶级处理程序可能只显示一般错误页面.但是,在大多数情况下,如果数据库处于脱机状态或缺少某些关键配置,则没有太多事情可做.

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

  • 我不同意.对于一段代码而言,捕获一般异常并将Message更改为更具体的内容是有意义的; 或检查前提条件.是的,他可以允许传播NullReferenceException,但这不会给调用者一个线索,即该异常是由于无法构建连接所致. (3认同)
  • 我不同意约翰.调用者确实会知道导致异常的原因,因为异常包括堆栈跟踪. (3认同)
  • 究竟.异常的经验法则是,如果由于配置或编码错误而抛出它,请不要捕获它.如果在用户输入期间偶尔会出现这种情况,请继续捕捉它.上面显示的"防御性编码"示例确实是"编码恐怖". (3认同)

mqp*_*mqp 13

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

如果您正在编写您期望被许多其他人使用的库代码,那些不会与您讨论如何使用它的库代码那么它就不会过度.他们会感激你的努力.

(也就是说,如果你这样做,我建议你定义比System.Exception更好的例外,以便让那些想要捕获一些例外而不是其他例外的人更容易.)

但是如果你只是想自己使用它(或者你和你的伙伴),那么显然它是过度的,并且可能最终会让你的代码不那么可读而伤害你.


bob*_*lex 7

我希望我能让我的团队像这样编码.大多数人甚至都没有得到防御性编程的观点.他们做的最好的事情是将整个方法包装在try catch语句中,并让所有异常都由泛型异常块处理!

向你致敬Ian.我能理解你的困境.我自己经历过同样的事情.但你所做的可能会帮助一些开发人员进行几个小时的键盘攻击.

请记住,当您使用.net框架API时,您对它的期望是什么?什么看似自然?对你的代码做同样的事情.

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

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


Rob*_*vey 6

您的"之前"示例具有清晰简洁的区别.

如果出现问题,框架最终会抛出异常.如果你无法对异常做任何事情,你也可以让它传播到调用堆栈.

但是,有时候,在框架内部抛出一个异常,实际上并没有说明实际问题是什么.如果您的问题是您没有有效的连接字符串,但框架会抛出"无效使用null"之类的异常,那么有时最好捕获异常并使用更有意义的消息重新抛出它.

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

  • 对于理解对象及其目的的人来说,未注释的代码是相当明显的. (3认同)