代码改变世界

reading--<Effective C# -- 利用using和try/finally语句来清理资源>

2013-04-11 16:55  十行[Arist]  阅读(241)  评论(2编辑  收藏  举报

前言

    在c#中,我们通常使用IDisposable接口中的Dispose()方法来显示释放使用非托管系统资源的类型。.NET环境规定,这样的调用责任由使用类型的代码来承担,而非类型本身或系统来承担。因此,只要使用拥有Dispose()方法的类型,我们都有责任去调用它来释放其拥有的资源。确保调用Dispose()方法的最佳方式是使用using语句或者try/finally语句块。

    所有拥有非托管资源的类型都会实现IDisposable接口,另外作为一种保险措施,它们还会创建终接器,以防止我们忘记调用Dispose()方法。如果我们忘记调用Dispose()方法,其中的非内存资源会在随后终接器执行时被释放。这样对象在内存中存留的时间会比较长,应用程序对资源的清理也比较慢。

    但是C#语言添加了一些关键字来帮助我们轻松地处理此类事情,下面通过代码样例来说明。

 正文

   假设我们编写了如下的代码:

        public void ExecuteCommand(string connStr, string commandStr)
        {
            SqlConnection con = new SqlConnection(connStr);
            SqlCommand command = new SqlCommand(commandStr, con);

            con.Open();
            command.ExecuteNonQuery();
        }

         上面两个需要释放资源的对象:SqlConnection与SqlCommand,没有正确地调用Dispose()。这两个对象都存留在内存中,直到它们的终结器被调用。(两个类都继承了System.ComponentModel.Component类的终结器。)
          我们可以通过在完成了数据连接和命令之后,调用Dispose()方法来修正上述的问题:     

        public void ExecuteCommand(string connStr, string commandStr)
        {
            SqlConnection con = new SqlConnection(connStr);
            SqlCommand command = new SqlCommand(commandStr, con);

            con.Open();
            command.ExecuteNonQuery();

            command.Dispose();
            con.Dispose();
        }

          这看起来很好,但是如果执行SQL命令时抛出异常就比较麻烦了。如果抛出异常,Dispose()方法就永远不会被调用。C#中的using语句可以确保Dispose()方法被调用。我们可以在using语句中分配一个对象,C#编译器便会为每一个对象自动产生一个try/finally块。     

        public void ExecuteCommand(string connStr, string commandStr)
        {
            using (SqlConnection con = new SqlConnection(connStr))
            {
                using (SqlCommand command = new SqlCommand(commandStr, con))
                {
                    con.Open();
                    command.ExecuteNonQuery();
                }
            }
        }

          只要我们在一个函数中使用了Disposable对象,using语句都是确保正确释放对象资源的最简单的方法,using语句编译后会为被分配的对象产生一个try/finally块。例如,下面两段代码将产生同样的IL代码:         

 1   SqlConnection con = null;
 2  
 3   // using语句示例:
 4   using (con = new SqlConnection(connStr))
 5   {
 6        con.Open();
 7   }
 8  
 9   // try / finally语句块示例:
10   try
11   {
12        con = new SqlConnection(connStr);
13        con.Open();
14   }
15   finally
16   {
17        con.Dispose();
18   }

          如果我们对一个不支持IDisposable接口的对象使用using语句,C#编译器会产生错误。例如:

1  //下面的代码通不过编译。
2  // String是一个密封类,不支持IDisposable
3  using( string msg = "Hello world")
4      Console.WriteLine(msg);

         只有当编译时类型支持IDisposable接口时,using语句才会正常编译。因此,我们不能在using语句中随便使用对象:

1  // 下面的代码通不过编译
2  // Object 不支持IDisposable
3  using( object obj = Factory.CreateResource())
4      Console.WriteLine(obj.ToString());

         使用as语句可以帮助我们安全地释放那些可能支持(也可能不支持)IDisposable接口的对象:

1  //修正版本
2  // Object可能支持,也可能不支持IDisposable
3  Object obj = Factory.CreateResource();
4  using( obj as IDisposable)
5      Console.WriteLine(obj.ToString());

         如果obj实现率IDisposable接口,那么using语句会产生正确的资源释放代码。否则,using语句会退化为using(null)语句,这种做法很安全,只是不会做任何事情。如果我们不确定是否要将一个对象应用于using语句中,那么就像上面代码展示的那样选择最稳妥的做法:假定它需要资源清理,并将它放在using语句中。

         上面只是探讨一种简单的情况:只要在一个方法中使用了一个实现了IDisposable接口的对象,我们都要将其放在一个using语句中。现在,我们来看一种更复杂的用法。在前面的例子中,有两个不同的对象需要释放:SqlConnection与SqlCommand。我们在那里创建了两个using语句,每个using语句中放一个需要释放的对象。每个using语句会产生一个不同try/finally块。就好像我们编写了如下的代码一样:

 1         public void ExecuteCommand(string connStr, string commandStr)
 2         {
 3             SqlConnection con = null;
 4             SqlCommand command = null;
 5             try
 6             {
 7                 con = new SqlConnection(connStr);
 8                 try
 9                 {
10                     command = new SqlCommand(commandStr,con);
11 
12                     con.Open();
13                     command.ExecuteNonQuery();
14                 }
15                 finally
16                 {
17                     if (command != null)
18                         command.Dispose();
19                 }
20             }
21             finally
22             {
23                 if (con != null)
24                     con.Dispose();
25             }
26         }

          每一次使用using语句都会创建一个嵌套的try/finally块。这是一个比较丑陋的构造,因此当分配多个需要释放资源的对象时,可以自己编写一个try/finally块。

 1         public void ExecuteCommand(string connStr, string commandStr)
 2         {
 3             SqlConnection con = null;
 4             SqlCommand command = null;
 5             try
 6             {
 7                 con = new SqlConnection(connStr);
 8                 command = new SqlCommand(commandStr, con);
 9 
10                 con.Open();
11                 command.ExecuteNonQuery();
12             }
13             finally
14             {
15                 if (command != null)
16                     command.Dispose();
17                 if (con != null)
18                     con.Dispose();
19             }
20         }

            然而,不要过于聪明而去创建一个带有as语句的using构造:

 1         public void ExecuteCommand(string connStr, string commandStr)
 2         {
 3             // 错误的做法。潜在的资源泄露
 4             SqlConnection con = new SqlConnection(connStr);
 5             SqlCommand command = new SqlCommand(commandStr, con);
 6             using (con as IDisposable)
 7             {
 8                 using (command as IDisposable)
 9                 {
10                     con.Open();
11                     command.ExecuteNonQuery();
12                 }
13             }
14         }

            这段代码看起来好像更清晰,但是其中实际上隐藏有一个细微的bug。如果SqlCommand()构造器抛出一个异常,那么SqlConnection对象永远都不会被释放。我们必须确保任何实现了IDisposable接口的对象都在using块或者try块内分配。否则,就可能出现资源泄露。
            到目前为止,我们已经处理了两种最明显的情况。当我们在一个方法中分配了一个需要释放资源的对象时,将其放在一个using语句中是确保释放其资源的最好做法。当我们在同一个方法中分配多个需要释放资源的对象时,我们可以创建多个using语句块,或者自己写一个try/finally块。

            在释放某些对象的资源时,一些细微的地方还需要我们注意。有些类型既支持Dispose()方法,也支持Close()方法来释放资源。SqlConnection就是这样的一个类。我们可以像下面这样来关闭SqlConnection:

 1         public void ExecuteCommand(string connStr, string commandStr)
 2         {
 3             SqlConnection con = null;
 4             try
 5             {
 6                 con = new SqlConnection(connStr);
 7                 SqlCommand command = new SqlCommand(commandStr, con);
 8 
 9                 con.Open();
10                 command.ExecuteNonQuery();
11             }
12             finally
13             {
14                 if(con != null)
15                     con.Close();
16             }
17         }

             上面的代码也会将数据库连接关闭,但是它与Dispose()方法的行为并不完全相同。Dispose()方法除了释放资源外,还会通知GC该对象不再需要执行终结操作。Dispose()方法通过调用GC.SuppressFinalize()来实现这一点。Close()方法一般不会这么做。因此,调用过Close()方法的对象仍然留在终结队列(finalization queue)上,虽然这个时候对象已经不需要终结操作了。显然,如果可能,我们应该优先选择调用Dispose()方法。
             Dispose()方法不会将对象从内存上删除,它只是让对象释放非托管资源。这意味着如果释放的是仍被使用的对象,我们可能会遇到一些麻烦。因此,我们不应该释放那些仍被程序其他地方引用的对象。

        结束语

             在某些方面,C#中的资源管理可能比C++中的更为困难。我们不能再依赖确定性终结来释放使用的每一个资源。但是,垃圾收集环境可以在很大程度上简化我们的工作。我们使用的绝大多数类型不必实现IDisposable接口。.NET框架中实现了IDisposable接口的类不到100个---总的类型数量要超出1500个。当使用的类型实现了IDisposable接口时,我们要记住释放它们。我们应该将它们放在using语句或者try/finally块中。不管使用的是哪种构造,我们都要确保对象在任何时候都会被正确地释放。

                     PS:本文引用于书籍《Effective C# -- 50 Specific Ways to Improve Your C#》中文版条款15的文章。之前只知道什么时候用using语句,但是详细原理自己讲不明白、理解模糊,故在此记录下来。