CA2202,如何解决这个问题?

101

请问有谁能告诉我如何从以下代码中删除所有CA2202警告?

public static byte[] Encrypt(string data, byte[] key, byte[] iv)
{
    using(MemoryStream memoryStream = new MemoryStream())
    {
        using (DESCryptoServiceProvider cryptograph = new DESCryptoServiceProvider())
        {
            using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptograph.CreateEncryptor(key, iv), CryptoStreamMode.Write))
            {
                using(StreamWriter streamWriter = new StreamWriter(cryptoStream))
                {
                    streamWriter.Write(data);
                }
            }
        }
        return memoryStream.ToArray();
    }
}

警告 7 CA2202 : Microsoft.Usage : 在方法'CrytpoServices.Encrypt(string, byte[], byte[])'中,对象'cryptoStream'可以被释放多次。为避免引发'System.ObjectDisposedException'异常,您不应该在一个对象上调用Dispose超过一次: 行数:34

警告 8 CA2202 : Microsoft.Usage : 在方法'CrytpoServices.Encrypt(string, byte[], byte[])'中,对象'memoryStream'可以被释放多次。为避免引发'System.ObjectDisposedException'异常,您不应该在一个对象上调用Dispose超过一次: 行数:34,37

要查看这些警告,您需要使用Visual Studio代码分析(这些不是C#编译器警告)。


1
此代码不会产生这些警告。 - Julien Hoarau
1
我在这个项目中没有收到任何警告(Warn level 4,VS2010)。如果有人在这个领域搜索问题,请附上警告文本。 - H H
29
"CAxxxx" 警告是由 "Code Analysis" 和 FxCop 生成的。 - dtb
这个警告不适用于所示代码 - 警告可以被抑制,以适应这种情况。一旦您审查了您的代码并同意该评估,请在您的方法上方放置此内容:"[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="BrainSlugs83 said so.")]" - 确保您的using块中有一个 "using System.Diagnostics.CodeAnalysis;" 语句。 - BrainSlugs83
12个回答

144

在这种情况下,您应该抑制警告。处理一次性资源的代码应该是一致的,您不需要关心其他类接管了您创建的一次性资源并调用Dispose

[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
public static byte[] Encrypt(string data, byte[] key, byte[] iv) {
  using (var memoryStream = new MemoryStream()) {
    using (var cryptograph = new DESCryptoServiceProvider())
    using (var cryptoStream = new CryptoStream(memoryStream, cryptograph.CreateEncryptor(key, iv), CryptoStreamMode.Write))
    using (var streamWriter = new StreamWriter(cryptoStream)) {
      streamWriter.Write(data);
    }
    return memoryStream.ToArray();
  }
}

更新:IDisposable.Dispose文档中,你可以读到这个说明:

如果一个对象的Dispose方法被调用多次,那么对象必须忽略第一次之后的所有调用。如果对象的Dispose方法被多次调用,它不得抛出异常。

有人认为这个规则存在是为了让开发者可以像上面展示的那样,在一系列可处理的情况下合理地使用using语句(或者这只是一个很好的副作用)。同样的道理,CA2202没有任何有用的目的,应该在整个项目中被禁止。真正的罪魁祸首是一个错误的Dispose实现,并且CA1065应该处理这个问题(如果它是你的责任)。


16
我认为这是fxcop中的一个错误,这个规则是不正确的。Dispose方法不应该抛出ObjectDisposedException异常,如果它确实抛出了异常,那么您应该在此时通过向实现Dispose方法的代码作者提交错误报告来解决问题。 - justin.m.chase
15
我同意在另一个帖子中@HansPassant的观点:该工具正在执行其工作,并警告您有关类的意外实现细节。个人而言,我认为真正的问题在于API设计本身。将嵌套类默认设定为假定可以获取在其他地方创建的另一个对象的所有权似乎是非常值得质疑的。我可以看出如果生成的对象将要返回,则这可能是有用的,但是默认假定这种情况似乎违反了正常的IDisposable模式,并且不符合直觉。 - BTJ
8
但是 MSDN 不建议抑制这种类型的消息。请参阅:https://msdn.microsoft.com/zh-cn/library/ms182334.aspx?f=255&MSPPError=-2147217396 - Adil Mammadov
2
谢谢链接 @AdilMammadov,有用的信息,但微软并不总是对这些事情正确。 - Tim Abell

41

这是准确的,这些流的Dispose()方法将被调用多次。StreamReader类将“拥有”CryptoStream,因此处理streamWriter的同时也会处理cryptoStream的Dispose。同样,CryptoStream类接管了MemoryStream的责任。

这些并不是真正的错误,这些.NET类对于多个Dispose()调用是具有弹性的。但是,如果你想要摆脱警告,则应该对这些对象放弃使用using语句。当代码抛出异常时,你需要进行一些痛苦的推理来确定会发生什么。或者使用属性来关闭警告。或者只需忽略这个愚蠢的警告。


11
如果要设计可重用的API,需要对类的内部行为(例如一个可被清除的对象取得了另一个对象的所有权)具有特殊的知识,这是难以承受的要求。因此,我认为using语句应该保留。这些警告真的很愚蠢。 - Jordão
4
@Jordão - 这难道不是该工具的作用吗? 它可以警告您可能不知道的内部行为? - Hans Passant
8
我同意。但是,我仍然不会放弃using语句。依靠另一个对象来处理我创建的对象的释放感觉很不妥。对于这段代码来说,没问题,但是在许多StreamTextWriter的实现中(不仅仅是BCL),使用它们的代码应该是一致的。 - Jordão
3
同意Jordão的观点。如果你真的希望程序员了解API的内部行为,那么通过将函数命名为DoSomethingAndDisposeStream(Stream stream,OtherData data)来说明。 - ZZZ
5
@HansPassant,您能否指出在哪里有记录XmlDocument.Save()方法会调用所提供参数的Dispose?我在Save(XmlWriter) 的文档中没有看到(我在这个方法中遇到了FxCop错误),也没有在Save() 方法本身或XmlDocument 的文档中看到。 - Ian Boyd
显示剩余3条评论

9
当一个 StreamWriter 被释放时,它会自动释放被包装的 Stream(这里是 CryptoStream)。CryptoStream 也会自动释放被包装的 Stream(这里是 MemoryStream)。

因此,您的 MemoryStreamCryptoStreamusing 语句同时释放。而您的 CryptoStream 则被 StreamWriter 和外部的 using 语句释放。


经过一些实验,似乎完全消除警告是不可能的。理论上,MemoryStream 需要被处理掉,但是这样理论上你就不能再访问它的 ToArray 方法了。实际上,一个 MemoryStream 不需要被处理掉,所以我会选择这个解决方案并抑制 CA2000 警告。
var memoryStream = new MemoryStream();

using (var cryptograph = new DESCryptoServiceProvider())
using (var writer = new StreamWriter(new CryptoStream(memoryStream, ...)))
{
    writer.Write(data);
}

return memoryStream.ToArray();

然后您理论上就不能再访问它的 ToArray 方法了。因此,将 return 行移到 using 块的右括号之前。 - Ben Voigt

9
我会使用#pragma warning disable来完成此操作。
.NET Framework指南建议以这样的方式实现IDisposable.Dispose,使其可以被多次调用。从IDisposable.Dispose的MSDN说明中可以看出:
块引用:

如果多次调用Dispose方法,则对象不得抛出异常。

因此,警告似乎几乎没有意义:
块引用:

为了避免生成System.ObjectDisposedException,在对象上不应调用Dispose超过一次。

我想可以这样说,如果您正在使用未按照标准实现指南的IDisposable对象,则该警告可能有所帮助。但是,当您像使用.NET Framework中的类时,使用#pragma来禁止警告是安全的。在我看来,这比按照MSDN文档中针对此警告的建议做更好的选择。

4
CA2202是代码分析警告而不是编译器警告。#pragma warning disable只能用于抑制编译器警告。要抑制代码分析警告,您需要使用属性。 - Martin Liversage

2

我在我的代码中遇到了类似的问题。

看起来整个CA2202的问题是由于如果构造函数中发生异常,MemoryStream可以被处理掉(CA2000)。

可以通过以下方式解决这个问题:

 1 public static byte[] Encrypt(string data, byte[] key, byte[] iv)
 2 {
 3    MemoryStream memoryStream = GetMemoryStream();
 4    using (DESCryptoServiceProvider cryptograph = new DESCryptoServiceProvider())
 5    {
 6        CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptograph.CreateEncryptor(key, iv), CryptoStreamMode.Write);
 7        using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
 8        {
 9            streamWriter.Write(data);
10            return memoryStream.ToArray();
11        }
12    }
13 }
14
15 /// <summary>
16 /// Gets the memory stream.
17 /// </summary>
18 /// <returns>A new memory stream</returns>
19 private static MemoryStream GetMemoryStream()
20 {
21     MemoryStream stream;
22     MemoryStream tempStream = null;
23     try
24     {
25         tempStream = new MemoryStream();
26
27         stream = tempStream;
28         tempStream = null;
29     }
30     finally
31     {
32         if (tempStream != null)
33             tempStream.Dispose();
34     }
35     return stream;
36 }

请注意,我们必须在最后一个using语句(第10行)中返回memoryStream,因为cryptoStream会在第11行被处理(因为它被用于streamWriter using语句中),这导致memoryStream也会在第11行被处理(因为memoryStream被用于创建cryptoStream)。
至少对我来说,这段代码是有效的。
编辑:
可能听起来有趣,但我发现如果您将GetMemoryStream方法替换为以下代码:
/// <summary>
/// Gets a memory stream.
/// </summary>
/// <returns>A new memory stream</returns>
private static MemoryStream GetMemoryStream()
{
    return new MemoryStream();
}

您可以获得相同的结果。

1
我希望能以正确的方式解决这个问题 - 即不抑制警告并正确处理所有可处理对象。
我将其中2个流作为字段提取出来,并在我的类的Dispose()方法中处理它们。是的,实现IDisposable接口可能不一定是您要寻找的,但与代码中所有随机位置的dispose()调用相比,该解决方案看起来非常干净。
public class SomeEncryption : IDisposable
    {
        private MemoryStream memoryStream;

        private CryptoStream cryptoStream;

        public static byte[] Encrypt(string data, byte[] key, byte[] iv)
        {
             // Do something
             this.memoryStream = new MemoryStream();
             this.cryptoStream = new CryptoStream(this.memoryStream, encryptor, CryptoStreamMode.Write);
             using (var streamWriter = new StreamWriter(this.cryptoStream))
             {
                 streamWriter.Write(plaintext);
             }
            return memoryStream.ToArray();
        }

       public void Dispose()
        { 
             this.Dispose(true);
             GC.SuppressFinalize(this);
        }

       protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.memoryStream != null)
                {
                    this.memoryStream.Dispose();
                }

                if (this.cryptoStream != null)
                {
                    this.cryptoStream.Dispose();
                }
            }
        }
   }

1

加密流基于内存流。

看起来发生的情况是,当加密流被处理(在使用结束时)时,内存流也被处理,然后再次处理内存流。


0

我只是想展开代码,这样我们就可以看到多个对象上对 Dispose 的多次调用。事实上,您确实在两个对象上调用了Dispose

memoryStream = new MemoryStream()
cryptograph = new DESCryptoServiceProvider()
cryptoStream = new CryptoStream()
streamWriter = new StreamWriter()

memoryStream.Dispose(); //implicitly owned by cryptoStream
cryptoStream.Dispose(); //implicitly owned by streamWriter
streamWriter.Dispose(); //through a using

cryptoStream.Dispose(); //INVALID: second dispose through using
cryptograph.Dispose(); //through a using
memorySTream.Dipose(); //INVALID: second dispose through a using

return memoryStream.ToArray(); //INVALID: accessing disposed memoryStream

虽然大多数.NET类(希望如此)都能够抵御对.Dispose的多次调用的错误,但并不是所有类都能够防范程序员的误用。

是的,规范文档说所有类必须免疫于程序员对.Dispose的多次调用的误用:

如果多次调用其Dispose方法,则对象不得引发异常

但这是现实世界 - 我们正在努力消除错误;而不是引起它们。FX Cop知道这一点,并警告您。

您有几个选择:

  • 只在任何对象上调用一次Dispose;不使用using
  • 继续调用两次dispose,并希望代码不会崩溃
  • 忽略该警告

0

虽然不是主题,但我建议您使用不同的格式技术来分组using

using (var memoryStream = new MemoryStream())
{
    using (var cryptograph = new DESCryptoServiceProvider())
    using (var encryptor = cryptograph.CreateEncryptor(key, iv))
    using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
    using (var streamWriter = new StreamWriter(cryptoStream))
    {
        streamWriter.Write(data);
    }

    return memoryStream.ToArray();
}

我也主张在这里使用var,以避免重复使用非常长的类名。

P.S. 感谢@ShellShock指出我不能省略第一个using的大括号,因为这会使return语句中的memoryStream超出范围。


5
memoryStream.ToArray() 不会超出作用域吗? - Polyfun
这与原始代码完全等价。我只是省略了花括号,就像您可以在 if 语句中做的那样(尽管我不建议在除 using 之外的任何情况下使用此技术)。 - Dan Abramov
2
在原始代码中,memoryStream.ToArray() 在第一个 using 的范围内;而你将它放到了范围之外。 - Polyfun
非常感谢,我刚意识到你是指“返回”语句。太对了。我编辑了答案以反映这一点。 - Dan Abramov
我个人认为,在代码中使用不带大括号的 using 会使其更加脆弱(考虑多年的差异和合并)。https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/ & https://www.imperialviolet.org/2014/02/22/applebug.html - Tim Abell

-1
避免使用全部命名空间,而是使用嵌套的Dispose-Calls!
    public static byte[] Encrypt(string data, byte[] key, byte[] iv)
    {
        MemoryStream memoryStream = null;
        DESCryptoServiceProvider cryptograph = null;
        CryptoStream cryptoStream = null;
        StreamWriter streamWriter = null;

        try
        {
            memoryStream = new MemoryStream();
            cryptograph = new DESCryptoServiceProvider();
            cryptoStream = new CryptoStream(memoryStream, cryptograph.CreateEncryptor(key, iv), CryptoStreamMode.Write);
            streamWriter = new StreamWriter(cryptoStream);

            streamWriter.Write(data);
            return memoryStream.ToArray();
        }
        finally 
        {
            if(streamWriter != null)
                streamWriter.Dispose();
            else if(cryptoStream != null)
                cryptoStream.Dispose();
            else if(memoryStream != null)
                memoryStream.Dispose();

            if (cryptograph != null)
                cryptograph.Dispose();
        }
    }

1
请解释为什么在这种情况下应避免使用 using - StuperUser
1
你可以将using语句放在中间,但必须解决其他的。为了得到一个逻辑连贯且在所有方向上可升级的解决方案,我决定删除所有的using语句! - Harry Saltzman

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