代码分析规则CA2000/CA2202

6

我试图确保我的编码遵循正确的对象处理方式,因此我将这些规则强制执行为错误。但是我在这段代码中遇到了问题。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Xml;

class MyClass
{  
    public String ToXml()
    {
        var objSerializer = 
            new DataContractSerializer(GetType());
        var objStream = new MemoryStream();
        StreamReader objReader;

        String strResult;
        try
        {
            // Serialize the object
            objSerializer.WriteObject(objStream, this);

            // Move to start of stream to read out contents
            objStream.Seek(0, SeekOrigin.Begin);

            objReader = new StreamReader(objStream);

            try
            {
                // Read Contents into a string
                strResult = objReader.ReadToEnd();
            }
            finally
            {
                objReader.Dispose();
            }
        }
        finally
        {
            if (objStream != null)
            {
                // objStream.Dispose();
            }
        }

        return strResult;
    }
}

如果我将objStream.Dispose()注释掉,会出现CA2000错误,因为我没有处理该对象;但是如果我去掉备注,则会出现多次处理的错误。
还有什么东西在处理这个对象吗?或者在处理多个流时我做错了?

为了可读性,我更喜欢使用块(using-blocks)而不是显式的 try-finally {dispose} - Albin Sunnanbo
我同意,最初它确实存在问题,我一直在努力摆脱错误。 - Dreamwalker
3个回答

8

这种情况也一直困扰着我。每隔几年,我都会运行fxcop或Visual Studio中现在内置的代码分析,来更新自己对“规则”的理解。

我最初编写了这段代码,并认为使用using正确释放资源是一个好习惯:

using (MemoryStream msDecrypt = new MemoryStream())
{
    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
    {
        csDecrypt.Write(eop.m_Ciphertext, 0, eop.m_Ciphertext.Length);
    }

    decrypted = msDecrypt.ToArray();
}

这段代码会导致CA2202 "不要多次处理对象"。这个规则的讽刺之处在于,它实际上不是关于你代码中的问题,而是为了保护你免受别人代码中的问题。Microsoft一直有很多有关如何实现Dispose模式(http://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.110).aspx)的文档。然而,通过查看此代码分析规则的详细信息(http://msdn.microsoft.com/en-us/library/ms182334.aspx),可以揭示出此规则的目的:

"一个正确实现的Dispose方法可以被多次调用而不会引发异常。但是,这并不是保证,为了避免生成System.ObjectDisposedException,您不应该在一个对象上调用Dispose超过一次。"

简而言之,这个规则的目的是保护自己免受不遵循规则的人的影响。

当然,我修改了代码,使其看起来像这样:

MemoryStream msDecrypt = new MemoryStream()    
//using (MemoryStream msDecrypt = new MemoryStream())
//{
    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
    {
        csDecrypt.Write(eop.m_Ciphertext, 0, eop.m_Ciphertext.Length);
    }

    decrypted = msDecrypt.ToArray();
//}

现在在这篇Stack Overflow文章上,大家都痛苦地意识到了一个新的问题,我们的朋友"CA2000“Dispose objects before losing scope”。所以此时我只是愣了一下。进行了几次谷歌搜索,并找到了这篇文章。这时我恍然大悟,为了通过这两个CA规则,你需要确保所有代码分支中的对象仅被处理一次。因此,我开始解决这个问题,一旦你意识到这就是你需要做的事情,这并不是一个难题。
自然而然地,代码演变成了这样:
MemoryStream msDecrypt = null;
CryptoStream csDecrypt = null;

try
{    
    msDecrypt = new MemoryStream();
    csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write);

    csDecrypt.Write(eop.m_Ciphertext, 0, eop.m_Ciphertext.Length);
    csDecrypt.FlushFinalBlock();
    decrypted = msDecrypt.ToArray();
}
finally
{
    if (csDecrypt != null)
    {
        csDecrypt.Dispose();
    }
    else if (msDecrypt != null)
    {
        msDecrypt.Dispose();
    }

}

最后,我的代码不再出现CA2000或CA2202错误。故事的寓意是,随着代码分析规则的发展,USING语句的价值远不及过去。
有几种不同的编写代码的方式可以使其工作,我只选择了一种不将显式调用dispose与using语句混合使用的方式,因为我认为这样更易于阅读,并且结构化得以防止其他人无意中添加另一个using语句导致原本的问题重现。

2
虽然从技术上讲,这种泛型方式是正确的,但实际应用证明使用更整洁的"using"结构不会有任何问题(即在成功时进行两次释放)。因此,为了可读性,我只会在知道类正确处理该错误时才将其正确抑制。我认为这就是MS代码分析警告的要点(让我们思考一下)。最好的解决方案是让代码分析规则引擎自己发现这一点;为我们提供一些声明这一点的方法(例如代码合同)。无论如何,我还是投了你的答案,因为这是一个很好的解释,必须被考虑到。 - Tony Wall

1

如果您处理了StreamReader,则也处置了底层流。

如果您注释掉objStream.Dispose(),那么在进入嵌套的try块之前可能会遇到某些异常抛出的机会 - 这将导致您的流未被处置。

这里有一个很好的解释: 处置streamreader是否关闭流?


是的,我理解了。如果你尝试运行这段代码,你会发现你正在两次处理objStream,但在我的实际代码中并没有这样做。也许是StreamReader.Dispose同时处理了传入的流? - Dreamwalker
1
在我的意见中,这是StreamReader的一个奇怪设计缺陷,并且经常让我感到困扰,因为有时候你并不想关闭所提供的流。 - Steven
@Zonder 是的,这正是我想说的。如果你处理一个读取器(或写入器),那么你也会处理底层流。所以在你的代码中(取消最后一个dispose的注释),它最终仍然被处理了两次。 - Mel
@Steven 我同意。而且围绕它进行设计很困难。通常我最终会处理掉流和读取器,这感觉有点多余。如果我加入一个写入器,那就更复杂了。 - Mel
嗯,好的,这很尴尬,因为这两个代码分析规则都被视为错误。唯一的解决办法是将CA2202设置为警告,并希望程序员记得捕获ObjectDisposedException异常。也许IDisposed应该有一个IsDisposed属性,但这会导致异步处理问题。 - Dreamwalker

0
这不是答案,但您可能会发现这段代码更易读:
public String ToXml()
{
    var objSerializer =
        new DataContractSerializer(GetType());

    using (var objStream = new MemoryStream())
    {
        //  Serialize the object
        objSerializer.WriteObject(objStream, this);

        // Move to start of stream to read 
        // out contents
        objStream.Seek(0, SeekOrigin.Begin);

        using (var objReader =
            new StreamReader(objStream))
        {
            // Read Contents into a string
            retirm objReader.ReadToEnd();
        }
    }
}

根据http://msdn.microsoft.com/en-us/library/ms182334,这正是不应该做的事情,因为可能/将会在objStream对象上调用Dispose()两次。 - mattanja
另一方面:Jon Skeet说这是可以的 - 所以它是合法的。https://dev59.com/THNA5IYBdhLWcg3wKam3#1065196 - mattanja
2
多次调用Dispose应该不是问题(除了可能会影响性能),因为IDisposable的规范说明必须能够处理这种情况。MSDN文档已经指出:“正确实现的Dispose方法可以被多次调用而不会抛出异常。”从这个意义上说,CA2202规则非常奇怪,或者至少应该归类为Microsoft.Performance而不是Microsoft.Usage类别下。 - Steven

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