《Effective C#》读书笔记——条目15:使用using和try/finally清理资源<.NET资源管理>
在.NET环境中,非托管系统资源由开发人员来负责释放,且非托管系统资源必须显式的使用IDisposable接口的Dispose()来释放(详见:了解.NET内存管理机制)。所有封装或使用了非托管资源的类型都实现了IDisposable接口。同时这些类也在终结器中调用Dispose(),保证了开发人员在忘记资源释放的时候仍然能够正常的释放掉资源(对象资源会一直停留在内存中,直到终结器被调用),这会导致资源在内存中停留的时间更长,导致应用程序会占用更多的系统资源。
阅读目录:
1.使用 using 关键字
在C#中为我们添加了一个现实释放非托管资源的关键字:using。using语句其实是一个C#语言的语法糖,当我们在using语句中分配可释放对象时,C#编译器将会自动在每个对象外生成一个try/finally块来包裹住分配的对象,保证资源的及时释放,即使抛出了异常也一样。如果要使用一个可销毁的对象,使用using语句能够以最简单的方式保证你的对象可以正常销毁。看下面使用了using语句的代码:
1 SqlConnection myConnection = null; 2 3 using (myConnection = new SqlConnection(connectionString)) 4 { 5 myConnection.Open(); 6 }
这段代码和下面的代码生成的IL代码完全一致:
1 try 2 { 3 myConnection = new SqlConnection(connectionString); 4 myConnection.Open(); 5 } 6 finally 7 { 8 myConnection.Dispose(); 9 }
使用using语句的注意事项:
如果using语句中分配的变量的类型没有实现IDisposable接口,编译器将会抛出异常。
1.1 安全销毁对象
对于一些可能实现或未实现IDisposable接口的对象,或者无法确定是否应该用using语句包裹某个对象时,由于其不确定性我们可以使用as操作符进行安全的销毁,如下:
1 object obj = Factory.CreateResource(); 2 //如果不确定obj是否实现了IDisposable接口,下面是安全的销毁方式 3 using (obj as IDisposable) 4 { 5 Console.WriteLine(obj.ToString()); 6 }
如果obj实现了IDisposable接口,那么using语句将生成清理代码;反之,using语句将变成using(null) ,这并不会抛出异常,也不会有任何其他意料之外的操作。
2.同时销毁多个可销毁对象
通过前面我们知道使用using语句时编译器会将其转换为try/finally语句,这能够保证我们可以正确的将非托管对象销毁。但是当我们想要同时销毁多个对象时情况会有一点细微的变化,看下面的代码:
1 public void ExecuteCommand(string connString, string commandString) 2 { 3 using (SqlConnection myConnection = new SqlConnection(connString)) 4 { 5 using (SqlCommand mySqlCommand = new SqlCommand(commandString, myConnection)) 6 { 7 myConnection.Open(); 8 mySqlCommand.ExecuteNonQuery(); 9 } 10 } 11 }
我们看到方法内部有两个非托管对象需要被释放,上面的代码运行的效果和我们的预期并没有差别,我们通过查看IL代码发现,这个示例生成的IL和下面的示例是一样的:
1 public void ExecuteCommand(string connString, string commandString) 2 { 3 SqlConnection myConnection = null; 4 SqlCommand mySqlCommand = null; 5 try 6 { 7 myConnection = new SqlConnection(connString); 8 try 9 { 10 mySqlCommand = new SqlCommand(commandString, myConnection); 11 12 myConnection.Open(); 13 mySqlCommand.ExecuteNonQuery(); 14 } 15 finally 16 { 17 if (mySqlCommand != null) 18 mySqlCommand.Dispose(); 19 } 20 } 21 finally 22 { 23 if (myConnection != null) 24 myConnection.Dispose(); 25 } 26 }
上面的代码并没有上面问题,运行效果可以达到了我们的预期,不过我们可以做得好点,直接使用try/finally语句,避免在这样情况中使用using语句而生成了多余的try/finally语句,这样写会更好:
1 public void ExecuteCommand(string connString, string commandString) 2 { 3 SqlConnection myConnection = null; 4 SqlCommand mySqlCommand = null; 5 try 6 { 7 myConnection = new SqlConnection(connString); 8 mySqlCommand = new SqlCommand(commandString, myConnection); 9 10 myConnection.Open(); 11 mySqlCommand.ExecuteNonQuery(); 12 } 13 finally 14 { 15 if (mySqlCommand != null) 16 mySqlCommand.Dispose(); 17 if (myConnection != null) 18 myConnection.Dispose(); 19 }
注意:
必须保证每个实现了IDisposable接口的对象都放在了using或try/finally中,否则就可能会发生资源泄露。
3.释放可销毁对象的方式
我们发现在我们释放可销毁对象时。有的类型不但提供Dispose()方法还提供了一个Close方法,比如前面示例中SqlConnection类的myConnection对象,我们可以知道调用myConnection对象的Close方法关闭数据库连接,但是这和调用它的Dispose()有一些差别:Dispose()方法将调用GC.SuppressFinalize()方法,而Close()方法一般则不会,因此,即使已经不需要终结,但对象仍旧在终结队列中。如果两种方式你可以选择,应该优先使用Dispose方法。
同时我们需要知道:Dispose()并不是将对象从内存中移除,而只是让对象释放掉其中的非托管资源。
小节
.Net Framework 中只有不到100个类实现了IDisposable接口,当我们使用实现了IDisposable接口的类时,我们要保证能够正确的进行清理工作,可以将这些对使用using语句或者是try/finally语句包裹起来。无论使用哪种方式,都要保证对象在任何时候、任何情况下都被正确的销毁。