嵌套使用Using Statement造成对象被dispose多次 CA2202

前言:

Using语句的用途有好几种,这几种用法都很简单。

我们这里说的C# Using语句是用来释放资源的那个,不是引用的那个。

这里不是教大家如何使用using,只是ColdJokeLife在使用的过程中出现了一点问题,在这里分享一下。

ColdJokeLife在使用FxCop过程中,遇到的CA2202问题,下面分享一下解决过程和分析结果。

没有什么高深的技术,园中的老牛、大虾们可以绕行了。

 

一、Using简介

使用Using语句来释放资源的原理很简单,使用try/finally包裹代码段,并在代码块结尾,调用对象的Dispose方法。

例如:

/*代码取自MSDN官网*/
// 使用using
using (Font font1 = new Font("Arial", 10.0f)) 
{
    byte charset = font1.GdiCharSet;
}

// 等效代码
{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

 

二、出现的问题

前面已经简要介绍了using 的一些用法,我的问题是更复杂一点点的:嵌套使用using。

示例代码如下:

// nested using statements
using (Stream stream = new FileStream("file.txt", FileMode.OpenOrCreate))
{
    using (StreamWriter writer = new StreamWriter(stream))
    {
        // Use the writer object...
    }
}

上例,嵌套使用了using语句,看上去貌似没有什么问题,但是这里会有bug。

bug原因:当内层using代码段执行完毕后,会释放writer,但此时stream也被释放了。

所以,当外层using代码段执行完毕后,会造成stream被释放两次。这就出现问题了。

 

补充一点:按照正常Dispose的实现要求,实现Dispose方法使其被多次调用时,不会出现错误。

所以,如果正确实现Dispose方法,那么这里的问题就不是错误。

所以,这里这里只是一个隐患,不一定会出现问题。

 

三、解决办法

这里的解决办法非常简单就是修改代码,不使用嵌套using的方式,自己写try/finally代码段。

// 解决办法:自己写try/finally
Stream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.OpenOrCreate);
    using (StreamWriter writer = new StreamWriter(stream))
    {
        stream = null; // 必须在最开始的位置,原因后面讲 
     //Use the writer object...
} } finally { if(stream != null) stream.Dispose(); }

 

四、原因分析

最开始,我猜想是using在代码结束时,将用到的所有对象都释放了,包括外层using中定义的对象。

但是这种猜想不太可能,因为前面对using的分析看来,编译器不可能去做这些多余的事情,它只会帮你释放using创建的那些对象。

去度娘、谷哥、StackOverflow都没有找到想要的答案,就去看内层Using中创建的StreamWriter的Dispose方法,

果然找到了答案!

StreamWriter的Dispose方法中一段这样的代码:

// 如果LeaveOpen为false,那么就释放掉steam对象
// 这里的stream是通过构造函数传入的,即我们例子中的FileStream
if (!this.LeaveOpen)
{
          if (this.stream != null)
          {
            try
            {
              if (disposing)
                this.stream.Close();
            }
            // ...
}

从这段代码中,就可以看出来是内层的对象在Dispose方法时,帮我们把外层的对象也释放了。

这是StreamWriter的机制,不是using嵌套的问题。

所以,我们再来看看刚才写的解决办法:

1、将外层using换成我们写的try/finally

  这么做是为了防止嵌套的时候,外层using再次去dispose造成问题。

  如果没有出现异常,内层using正常dispose streamWriter时,stream也被dispose,而且stream也被设置为null。

  所以不会再被dispose。如果出现异常,streamWriter没有被正常dispose,那么保证stream可以在finally中被释放。

2、stream = null; 语句必须放在最前面

  因为假如内层using中出现异常,因为using会包裹一层try/finally,所以streamWriter肯定会被dispose,所以stream不用被dispose。

  所以,只要正常运行到using代码块内,说明不用再去释放stream了,那么就必须把它设置为null,防止多次dispose。

 

五、总结

1、嵌套的using并不是不能使用

  产生多次Dispose的原因,是内层对象(例如:StreamWriter)的Dispose机制(LeaveOpen)。

  它与using的嵌套无关,所以如果内层对象与外层对象间无关系的话,应该还是可以正常使用的。

 

2、使用嵌套using要小心

  当我们不太清除内层对象是否与外层对象有关系时,需要小心,因为一旦不注意,就会造成外层对象被多次释放。

  而且在多层次嵌套时,更加要小心。

 

最后,贴上一个4层嵌套的例子:

// 4层嵌套代码
using (var cryptoProvider = new DESCryptoServiceProvider())
{
       using (var ms = new MemoryStream(byEnc))
       {
              using (var cst = new CryptoStream(ms, cryptoProvider.CreateDecryptor(byKey, byIv), CryptoStreamMode.Read))
              {
                    using (var sr = new StreamReader(cst))
                    {
                            // 业务逻辑
                    }
               }
        }
}

// 修改后的代码
DESCryptoServiceProvider cryptoProvider = null;
MemoryStream memoryStream = null;
CryptoStream cryptoStream = null;

 try
{
         cryptoProvider = new DESCryptoServiceProvider();
         memoryStream = new MemoryStream(encryptData);
         cryptoStream = new CryptoStream(memoryStream, cryptoProvider.CreateDecryptor(rgbKey, rgbIv), CryptoStreamMode.Read);

         using (var streamReader = new StreamReader(cryptoStream))
         {
               memoryStream = null;
               cryptoStream = null;
// 业务逻辑
} } finally { if (cryptoProvider != null) { cryptoProvider.Dispose(); } if (cryptoStream != null) { cryptoStream.Dispose(); memoryStream = null; } if (memoryStream != null) { memoryStream.Dispose(); } }

自己查资料、分析,尝试后的一些东西,希望与大家分享一下。

希望对大家有帮助。

posted @ 2013-06-13 18:40  大海向西流  阅读(1547)  评论(2编辑  收藏  举报