问题 更正此代码的IDisposable实现


我有以下代码

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为Disposed时,由于构造函数的最后一个参数(leaveOpen = true),它会使底层Stream(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.

我不能赢...(除非我遗漏了一些明显的东西)我已经尝试了各种各样的事情,比如试试/最终围绕块,并在那里处理MemoryStream,但它要么说我正在处理两次,或根本没有!


8280
2017-11-18 12:43


起源

这很奇怪。从 msdn docs:[...] Dispose方法[...]应该可以多次调用而不会抛出异常(ObjectDisposedException)。 - oleksii
CA2000是一个巨大的皮塔饼。根据我的经验,它会产生比genuiune警告更多的误报。这一切 哭狼 现在意味着我倾向于在它出现时忽略/压制CA2000。 - LukeH
你不能赢。修复代码中的错误,gz需要Flush()或关闭以生成所有字节。 - Hans Passant


答案:


这有时是跑步的问题 CodeAnalysis,有时候你 根本无法取胜 你必须选择较小的邪恶。

在这种情况下,我认为正确的实现是第二个例子。为什么?根据 .NET Reflector, 实施 GZipStream.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:?)因为我不知道你发布的错误信息是什么。

HTH,

编辑:

@CodeInChaos:

深入了解实施情况 DeflateStream.Dispose  我相信它仍会为你处理MemoryStream而不管它调用的leaveOpen选项 base.Dispose() 

编辑 忽略上面的内容 DeflateStream.Dispose。我在Reflector中查看错误的实现。请参阅评论了解详情


1
2017-11-18 13:03



这看起来像是在转发处理内存流的工作 DeflateStream,这可能只会这样做 leaveOpen 参数为false。 - CodesInChaos
@CodeInChaos:更详细地更新了我的答案。我相信我已经正确地解释了代码。 - Dennis
它设定了 _stream 领域到 null 在打电话之前 base.Dispose。但在这种情况下,您发布的方法应该抛出...... - CodesInChaos
哪个类是最后一个方法? DeflateStream的基础是 Stream,这显然不可能 Stream处置。 - CodesInChaos
最后一种方法来自 System.IO.Stream。 - Dennis


除了处置问题,您的代码也被破坏了。您应该在回读数据之前关闭zip流。

还有一个 ToArray() 方法 MemoryStream,不需要自己实现。

using (MemoryStream ms = new MemoryStream())
{
    using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress,true))
    {
        gz.Write(CompressMe, 0, CompressMe.Length);
    }
    return ms.ToArray();
}

我只是压制警告,因为这是误报。代码分析可以为您服务,而不是相反。


6
2017-11-18 13:03



+1。我也相信错误是一个 假阳性 并在分析之后选择最正确的实现并禁止警告/规则。 - Dennis
+1特别是对于那些应该为我们工作的工具,而不是我们为他们工作的工具。 - Binary Worrier
谢谢你指出了ToArray()方法。 - Rich S
re:关闭zip流,我实际上是在我的原始代码中执行此操作,只是在我运行代码分析并尝试进行更改之后,这个小错误才进入.. - Rich S


这个页面在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();
}

这是尝试...最终在您的解决方案中缺少导致第二条消息。

如果这:

GZipStream gz = new GZipStream(ms, CompressionMode.Compress, false)

失败的流将不会被处置。


3
2017-11-18 12:48



我确实尝试了MSDN页面中的示例,但是它提出了相同的代码分析错误。 - Rich S
另外,如果GZip构造函数失败,它仍然应该正确地从“使用”块中退出..这个想法不是吗? - Rich S
这很奇怪......当我在答案中分析代码时,它不会产生任何错误/警告 - Erno de Weerd
@Enro:你或许有不同 CodeAnalysis 规则集启用比OP? - Dennis
我测试了代码 msdn.microsoft.com/en-us/library/... - Rich S


实际上,有效地在内存流上调用两次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;
        }
    }

2
2017-11-18 13:27





你必须去上学:

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警告(如果适用)。


0
2017-11-18 12:52



是的,我倾向于同意,我确信我可以解决这个问题,使用{}命令应该处理这样的事情并简化代码似乎很疯狂。 - Rich S
使用块是否是必要的,因为使用简单的试验/最终的合成糖?难道不是所有的处理都发生在finally块中吗? - Stephen Kennedy