C# 使用语句捕获错误

29

我只是在看using语句,我一直知道它的作用,但直到现在才尝试使用它,我编写了以下代码:


I am just looking at the using statement, I have always known what it does but until now not tried using it, I have come up with the below code:
 using (SqlCommand cmd = 
     new SqlCommand(reportDataSource, 
         new SqlConnection(Settings.Default.qlsdat_extensionsConnectionString)))
 {
     cmd.CommandType = CommandType.StoredProcedure;
     cmd.Parameters.Add("@Year", SqlDbType.Char, 4).Value = year;
     cmd.Parameters.Add("@startDate", SqlDbType.DateTime).Value = start;
     cmd.Parameters.Add("@endDate", SqlDbType.DateTime).Value = end;
     cmd.Connection.Open();

     DataSet dset = new DataSet();
     new SqlDataAdapter(cmd).Fill(dset);
     this.gridDataSource.DataSource = dset.Tables[0];
 }

这似乎有效,但如果我没有看错的话,我仍然需要将其包含在try catch块中以捕获未预见到的错误,例如 sql server 宕机。我有什么遗漏吗?

就我目前所看到的,它只是阻止我关闭和处理cmd,但由于仍然需要 try catch,所以代码行数将会更多。

16个回答

57

在进行IO工作时,我编写代码以预期出现异常。

SqlConnection conn = null;
SqlCommand cmd = null;

try
{
    conn = new SqlConnection(Settings.Default.qlsdat_extensionsConnectionString)
    cmd = new SqlCommand(reportDataSource, conn);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@Year", SqlDbType.Char, 4).Value = year;
    cmd.Parameters.Add("@startDate", SqlDbType.DateTime).Value = start;
    cmd.Parameters.Add("@endDate", SqlDbType.DateTime).Value = end;

        conn.Open(); //opens connection

    DataSet dset = new DataSet();
    new SqlDataAdapter(cmd).Fill(dset);
    this.gridDataSource.DataSource = dset.Tables[0];
}
catch(Exception ex)
{
    Logger.Log(ex);
    throw;
}
finally
{
    if(conn != null)
        conn.Dispose();

        if(cmd != null)
        cmd.Dispose();
}

编辑:明确地说,我避免在这里使用using块,因为我认为在这样的情况下记录日志非常重要。经验告诉我,你永远不知道会出现什么奇怪的异常。在这种情况下记录日志可能有助于检测死锁,或查找模式更改影响你代码库中未经过多次测试和使用的部分,或其他任何问题。

编辑2:在这种情况下,人们可以争论一个using块是否可以包装try/catch,并且这是完全有效且功能相等的。这实际上归结为个人喜好。你想避免额外的嵌套以换取处理自己的资源回收吗?还是你愿意承受额外的嵌套以获得自动回收?我觉得前者更加简洁,所以我这样做。但是,如果我发现代码库中有后者,我不会重写它。

编辑3:我真的非常希望微软创建一个更明确的using()版本,在这种情况下使其更具直观性并提供更多灵活性。考虑以下虚构代码:

SqlConnection conn = null;
SqlCommand cmd = null;

using(conn = new SqlConnection(Settings.Default.qlsdat_extensionsConnectionString),
          cmd = new SqlCommand(reportDataSource, conn)
{
    conn = new SqlConnection(Settings.Default.qlsdat_extensionsConnectionString);
    cmd = new SqlCommand(reportDataSource, conn);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@Year", SqlDbType.Char, 4).Value = year;
    cmd.Parameters.Add("@startDate", SqlDbType.DateTime).Value = start;
    cmd.Parameters.Add("@endDate", SqlDbType.DateTime).Value = end;
        cmd.Open();

    DataSet dset = new DataSet();
    new SqlDataAdapter(cmd).Fill(dset);
    this.gridDataSource.DataSource = dset.Tables[0];
}
catch(Exception ex)
{
    Logger.Log(ex);
    throw;
}

使用using语句只是创建了一个带有Dispose()调用的try/finally块。为什么不给开发人员提供一种统一的方法来处理释放和异常?


为什么要显式地进行处理,当使用块可以更加优雅地完成完全相同的事情呢? - yfeldblum
3
所以我可以记录异常。在一个遥远的国家/世界用户的桌面上出现运行时异常没有任何优雅之处,而你也不知道出了什么问题。 - Jason Jackson
3
明确的处理和使用'using'并不会对记录异常的方式/是否有影响。你可以轻松地使用'using'块,在其中编写一个try块来执行操作并编写一个catch块来记录异常。 - yfeldblum
5
我意识到使用 using 来处理异常存在缺陷。我同意 @Jason Jackson 的观点,编译时 using 会被转换为 try/finally。如果我想捕获异常,在 using 中添加 try/catch 将会生成两个 try 语句块,这会影响性能。更糟糕的是,.Dispose() 方法通常不会关闭连接,直到调用 .Close() 或等待垃圾回收。那么,为什么不直接写上 try/catch/finally 呢?我将在 final 块中检查空引用、连接是否打开并关闭和销毁它。这更加合适和更好。 - CallMeLaNN
1
@CallMeLaNN - 是的!这个问题困扰我很长时间了。我理解语言快捷方式与特定类型的关联,例如使用()与IDisposable的绑定,但我希望他们将其纳入更明确的内容中。请参见我上面的新示例。 - Jason Jackson
你在第一个代码块中为什么要显式地将它们初始化为null呢?为什么不在try块中进行初始化呢?SqlConnection conn = null; SqlCommand cmd = null; - Alex Gordon

18

为确保及时关闭连接,应将此代码编写如下。仅关闭命令不会关闭连接:

using (SqlConnection con = new SqlConnection(Settings.Default.qlsdat_extensionsConnectionString))
using (SqlCommand cmd = new SqlCommand(reportDataSource, con))
         {
             cmd.CommandType = CommandType.StoredProcedure;
             cmd.Parameters.Add("@Year", SqlDbType.Char, 4).Value = year;
             cmd.Parameters.Add("@startDate", SqlDbType.DateTime).Value = start;
             cmd.Parameters.Add("@endDate", SqlDbType.DateTime).Value = end;
             cmd.Connection.Open();

             DataSet dset = new DataSet();
             new SqlDataAdapter(cmd).Fill(dset);
             this.gridDataSource.DataSource = dset.Tables[0];
         }
为了回答你的问题,你可以在finally块中做相同的事情,但这样可以使代码范围更加清晰,并确保你记得清理。

14
如果你打算使用 try/catch/finally 块,那么在这种情况下使用 using 语句可能没有优势。如你所知,using 语句是 IDisposable 对象的一个包含 try/finally 的语法糖,用于释放资源。如果你要自己写 try/finally ,那么你可以自己调用 Dispose 方法。
这主要取决于编程风格 - 你的团队可能更习惯使用 using 语句,或者使用 using 语句可以使代码看起来更清晰。
但是,如果 using 语句将隐藏其本应具有的样板代码,那么如果你喜欢的话就自己处理吧。

6
如果你的代码长成这个样子:
using (SqlCommand cmd = new SqlCommand(...))
{
  try
  {
    /* call stored procedure */
  }
  catch (SqlException ex)
  {
    /* handles the exception. does not rethrow the exception */
  }
}

然后我会重构它,使用try..catch..finally。
SqlCommand cmd = new SqlCommand(...)
try
{
  /* call stored procedure */
}
catch (SqlException ex)
{
  /* handles the exception and does not ignore it */
}
finally
{
   if (cmd!=null) cmd.Dispose();
}

在这种情况下,我必须处理异常,所以除了加入try..catch外别无选择,我还可以添加finally子句并保存另一个嵌套层级。请注意,我必须在catch块中执行某些操作,而不是忽略异常。

5
详细说明了Chris Ballance所说的,C#规范(ECMA-334版本4)第15.13节规定:“using语句被翻译成三个部分:获取、使用和处理。资源的使用隐式地包含在一个包括finally子句的try语句中。这个finally子句会处理资源的释放。如果获取的资源为空,则不会调用Dispose,也不会抛出任何异常。”
这段描述接近两页纸 - 值得一读。
根据我的经验,SqlConnection/SqlCommand可能以多种方式生成错误,几乎需要更多地处理抛出的异常而不是处理预期的行为。我不确定是否应该在这里使用using语句,因为我想自己处理空资源情况。

4

"using"存在的一个问题是它不能处理异常。 如果"using"的设计者能够在其语法中添加可选的"catch",如下伪代码所示,那么它将更加有用:

using (...MyDisposableObj...)
{

   ... use MyDisposableObj ...

catch (exception)

   ... handle exception ...

}

it could even have an optional "finally" clause to cleanup anything other than the "MyDisposableObj" allocated at the beginning of the "using" statement... like:

using (...MyDisposableObj...)
{

   ... use MyDisposableObj ...
   ... open a file or db connection ...

catch (exception)

   ... handle exception ...

finally

   ... close the file or db connection ...

}

仍然不需要编写代码来处理MyDisposableObj,因为它将由using处理...

你觉得怎么样?


4

使用using关键字并不仅仅是捕获异常的问题,它更重要的是正确地处理那些无法被垃圾回收器管理的资源。


2
我明白你的意思,但我看不出与具有关闭和释放语句的try catch finally块相比的优势。 - PeteT
2
资源可能不在垃圾回收器的视野之外。尽早清理它们仍然有帮助,而不是等待垃圾回收。 - Jennifer Zouak
1
这也与垃圾收集器无关。 - John Saunders

2
这里有很多好的答案,但我认为还没有说到这一点。无论如何,在“using”块中,“Dispose”方法都将被调用。如果放置了一个返回语句或抛出错误,“Dispose”将被调用。
例如:
我创建了一个名为“MyDisposable”的类,它实现了IDisposable,并简单地执行了Console.Write。即使在所有这些情况下,它也总是写入控制台。
using (MyDisposable blah = new MyDisposable())
{
    int.Parse("!"); // <- calls "Dispose" after the error.

    return; // <-- calls Dispose before returning.
}

2

是的,您仍然需要捕获异常。使用块的好处在于为您的代码添加了范围。您在说:“在这个代码块内做一些事情,当它到达结尾时,关闭和处理资源”。

这完全不是必需的,但它确实定义了您对使用您的代码的任何其他人的意图,并且还有助于不会因错误而留下连接等未关闭的问题。


1

我会根据我所处理的资源来决定何时使用using语句,何时不使用。对于有限的资源,例如ODBC连接,我更喜欢使用T/C/F,这样我就可以在发生错误时记录有意义的错误。让数据库驱动程序错误回传到客户端并且可能在更高级别的异常包装中丢失是次优的。

T/C/F可以使你放心地处理资源。正如一些人已经提到的,using语句没有提供异常处理,它只确保资源被销毁。异常处理是一种被低估且被低效利用的语言结构,通常是解决方案成功与否的差异所在。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接