这段代码的正确IDisposable实现方式是什么?

11

我有如下代码

public static byte[] Compress(byte[] CompressMe)
{
    using (MemoryStream ms = new MemoryStream())
    {
        using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress,true))
        {
            gz.Write(CompressMe, 0, CompressMe.Length);
            ms.Position = 0;
            byte[] Result = new byte[ms.Length];
            ms.Read(Result, 0, (int)ms.Length);
            return Result;
        }
    }
}

这段代码可以正常运行,但当我对其进行代码分析时,会出现以下消息

CA2202 : Microsoft.Usage : Object 'ms' can be disposed more than once in 
method 'Compression.Compress(byte[])'. To avoid generating a 
System.ObjectDisposedException you should not call Dispose more than one 
time on an object.

就我个人而言,当GZipStream被Dispose掉时,由于构造函数的最后一个参数(leaveOpen=true),它将保留底层流(ms)的打开状态。

如果我稍微更改我的代码... 移除对MemoryStream的'using'块,并把'leaveOpen'参数改为false...

public static byte[] Compress(byte[] CompressMe)
{
    MemoryStream ms = new MemoryStream();
    using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress, false))
    {
        gz.Write(CompressMe, 0, CompressMe.Length);
        ms.Position = 0;
        byte[] Result = new byte[ms.Length];
        ms.Read(Result, 0, (int)ms.Length);
        return Result;
    }
}

然后会出现...

CA2000 : Microsoft.Reliability : In method 'Compression.Compress(byte[])',
object 'ms' is not disposed along all exception paths. Call 
System.IDisposable.Dispose on object 'ms' before all references to 
it are out of scope.
我赢不了..(除非我漏看了什么)我尝试了各种方法,例如在代码块中添加try/finally,并在其中Dispose MemoryStream,但要么说我正在两次处理它,要么根本没有处理!!

5
这句话的意思是:这很奇怪。根据MSDN文档,Dispose方法可以被多次调用而不会抛出异常(ObjectDisposedException)。请注意,这里并未提供解释或其他内容。 - oleksii
CA2000是一个非常讨厌的问题。在我的经验中,它产生的误报比真正的警告还要多。所有这些crying wolf现在意味着每当它出现时,我倾向于忽略/抑制CA2000。 - LukeH
1
你赢不了。请修复你代码中的错误,gz需要Flush()或关闭以输出所有字节。 - Hans Passant
5个回答

6
除了处理问题,你的代码也有问题。在读取数据之前应该关闭zip流。
此外,MemoryStream 上已经有一个ToArray()方法,不需要自己实现。
using (MemoryStream ms = new MemoryStream())
{
    using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress,true))
    {
        gz.Write(CompressMe, 0, CompressMe.Length);
    }
    return ms.ToArray();
}

我建议仅忽略此警告,因为它是虚警。代码分析的目的是为了帮助您服务,而不是相反。


我也认为这个错误是一个误报,在分析后选择最正确的实现并抑制警告/规则。 - Dennis
特别是这些工具本应为我们服务,而不是我们为它们服务的观点。 - Binary Worrier
关于关闭zip流的问题,我在原始代码中已经实现了这一点,只是在运行代码分析并尝试进行更改后,这个小错误不慎滑进去了。 - Rich S

3

MSDN上的这个页面来看,

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();
}

你的解决方案中缺少try...finally语句,导致第二个消息出现。
如果是这样的话:
GZipStream gz = new GZipStream(ms, CompressionMode.Compress, false)

如果失败,该流将不会被处理。


此外,如果GZip构造函数失败,它仍应正确退出“using”块..这不就是想要的吗? - Rich S
非常奇怪...当我分析我的答案中的代码时,没有产生任何错误/警告。 - Emond
1
@Enro:你可能启用了与OP不同的CodeAnalysis规则集? - Dennis
我测试了来自http://msdn.microsoft.com/en-us/library/system.io.compression.gzipstream(v=VS.90).aspx的代码。 - Rich S
不,我在我的答案中使用了这段代码,那是没问题的。然而,当我分析原帖中的代码时,它确实失败了。我怀疑是因为 GZipStream.Dispose 可能会同时释放 MemoryStream。 - Emond
显示剩余2条评论

2

实际上,在内存流上两次有效地调用dispose不会引起任何问题,因为在MemoryStream类中可以轻松编写代码以对此进行编码,并且在测试Microsoft时似乎已经这样做了。因此,如果您不想抑制规则,则另一种选择是将方法分成两个:

    public static byte[] Compress(byte[] CompressMe)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            return Compress(CompressMe, ms);
        }
    }

    public static byte[] Compress(byte[] CompressMe, MemoryStream ms)
    {
        using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress, true))
        {
            gz.Write(CompressMe, 0, CompressMe.Length);
            ms.Position = 0;
            byte[] Result = new byte[ms.Length];
            ms.Read(Result, 0, (int)ms.Length);
            return Result;
        }
    }

1

这就是运行 CodeAnalysis 时的问题,有时候你只能选择 lesser evil™。

在这种情况下,我认为正确的实现是第二个示例。为什么?根据 .NET ReflectorGZipStream.Dispose() 的实现将为您处理 MemoryStream,因为 GZipStream 拥有 MemoryStream

以下是 GZipStream 类的相关部分:

public GZipStream(Stream stream, CompressionMode mode, bool leaveOpen)
{
    this.deflateStream = new DeflateStream(stream, mode, leaveOpen, true);
}

protected override void Dispose(bool disposing)
{
    try
    {
        if (disposing && (this.deflateStream != null))
        {
            this.deflateStream.Close();
        }
        this.deflateStream = null;
    }
    finally
    {
        base.Dispose(disposing);
    }
}

由于您不想完全禁用规则,因此可以仅针对此方法使用CodeAnalysis.SupressMessage属性进行抑制。

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability ", "CA2000:?", Justification = "MemoryStream will be disposed by the GZipStream.")]

注意:您需要填写完整的规则名称(即CA2000:?),因为我不知道从您发布的错误消息中是什么。

希望对您有所帮助,

编辑:

@CodeInChaos:

深入研究DeflateStream.Dispose实现我认为它仍然会为您处理MemoryStream的Dispose,无论leaveOpen选项如何,因为它调用了base.Dispose()

编辑请忽略上述关于DeflateStream.Dispose的内容。我看错了Reflector中的实现。有关详细信息,请参见评论。


这看起来像是将内存流的处理工作转发给了DeflateStream,如果leaveOpen参数为false,它可能只会执行该操作。 - CodesInChaos
@CodeInChaos:我更新了我的答案并加入了更多细节。我相信我已经正确解释了这段代码。 - Dennis
在调用 base.Dispose 之前,它将 _stream 字段设置为 null。但在这种情况下,您发布的方法应该会抛出异常... - CodesInChaos
最后一个方法在哪个类中?DeflateStream的基类是Stream,显然这不可能是Stream的dispose方法。 - CodesInChaos
啊,这就解释了……我从反编译器中复制/粘贴了错误的代码,所以它实际上是 SyncStream 内部类的 Disposing(bool) 方法,而不是 System.IO.Stream 的。 - Dennis
显示剩余2条评论

0

你必须回到老派的方式:

public static byte[] Compress(byte[] CompressMe)
{
    MemoryStream ms = null;
    GZipStream gz = null;
    try
    {
        ms = new MemoryStream();
        gz = new GZipStream(ms, CompressionMode.Compress, true);
        gz.Write(CompressMe, 0, CompressMe.Length);
        gz.Flush();
        return ms.ToArray();
    }
    finally
    {
        if (gz != null)
        {
            gz.Dispose();
        }
        else if (ms != null)
        {
            ms.Dispose();
        }
    }
}

看起来很糟糕,我知道,但这是唯一平息警告的方法。

个人而言,我不喜欢编写这样的代码,所以只需在适当的情况下抑制多个Dispose警告,因为Dispose调用应该是幂等的(在这种情况下是这样的)。


是的,我倾向于同意,我相信我可以解决这个问题,只是使用 {} 命令来处理这种情况并简化代码似乎有些不可思议。 - Rich S
使用块是否必要,考虑到using只是try/finally的语法糖?所有的处理都可以在finally块中完成吗? - Stephen Kennedy

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