在VS2010代码分析期间,涉及到IDisposable和CA2000警告

4

我希望你能为我提供一些帮助和建议。以下是我简化后的类结构(涉及it技术):

public class Bar: IDisposable {...}

public abstract class FooBase: IDisposable
{
    Bar bar;
    bool disposed;

    internal FooBase(Bar bar)
    {
        this.bar=bar;
    }

    public void Dispose()
    {
         Dispose(true);
         GC.SupressFinalize(this);

    }

    protected void Dispose(bool disposing)
    {
         if (!this.disposed)
         {
             if (disposing)
             {
                 this.bar.Dispose();
             }

             this.disposed = true;
         }
    }
}

public FooA: Foo {...}
public FooB: Foo {...}

public static class FooProvider
{
    public static FooA GetFooA()
    {
       Bar bar = new Bar();
       ...
       return new FooA(bar);
    }

    public static FooB GetFooB()
    {
        Bar bar = new Bar();
        ...
        return new FooB(bar);
    }

    ...
}

当我在这个代码上运行代码分析时,FooProvider类中所有'CreateFooX()'方法都会出现警告CA2000。此警告给出以下消息:
Microsoft.可靠性:在方法'FooProvider.GetFooX()'中,在所有对它的引用超出范围之前,调用System.IDisposable.Dispose于对象'bar'。
微软建议永远不要忽略此警告,但我不确定它是否在警告实际存在于代码中的问题。的确,在任何我们考虑的'CreateFooX()'方法中,在去出作用域之前,'bar'没有被处理,但是一个对它的引用存在于'FooX'对象中,该对象最终将被处理并依次负责处理'bar'的处理。
是我没有正确理解处理程序应如何工作,并且代码中存在一些基本缺陷,还是我应该忽略这个警告?
编辑
由于一些评论,我尝试修改工厂方法如下:
public static class FooProvider
{
    public static FooA GetFooA()
    {
       Bar bar = null;

       try
       {
           bar =  new Bar();
           ...
           return new FooA(bar);
       }
       catch
       {
           if (bar != null) bar.Dispose();
           throw;
       }
    }

    ...
}

但我仍然收到相同的警告。我猜这只是一个误报,所以我可以忽略它并且安全。谢谢任何建议。

不知道你是否为了简洁而省略了它,但是不要忘记在FooBase中包含一个终结器来实际调用Dispose,以防它没有在代码中被显式调用。 - Massif
是的,我省略了它。不过还是谢谢你提醒我。 - InBetween
@Massif,@InBetween:如果 FooBase 只涉及托管资源,并且不直接处理任何非托管资源,则终结器应该是完全不必要的。 - LukeH
@LukeH:你说得完全正确。我应该指出它确实涉及到非托管资源,但它与问题并没有直接相关,所以我没有提到它。 - InBetween
我也讨厌这个误报,已经在这里开了一个工单:https://connect.microsoft.com/VisualStudio/feedback/details/779134。如果你愿意,就投一票吧。 - RobSiklos
4个回答

3

这是代码分析常见的误报。它无法真正理解你的代码内在的情况,因此只能给出一个笼统的答案。请小心处理,但如果你确定这是误报,可以安全地忽略它。


2

这不是误报。如果在创建Bar并将其传递给Foo构造函数之前抛出异常,会怎样呢?我看到有几条代码路径可能存在一个或多个对象未被处理。


好的观点。然而,即使GetFooX()只包含'Bar bar=new Bar()'后跟'return new FooX(bar)',您仍会收到相同的警告,这是没有意义的,因为new FooX(bar)不能抛出任何异常(它只设置构造函数内部对象的状态:this.bar=bar)。但现在我明白了警告来自哪里,因为在一般情况下,FooX初始化程序可能会抛出异常。 - InBetween
你能展示一下 GetFooX 的调用者吗?他们使用了 using 吗?如果是这样的话,那么我会认为这是一个误报,并且不会责怪 VS,因为它很难确定没有任何代码路径不会被处理。 - John Saunders
所有调用 GetFooX 的程序集都在另一个程序集中,因此 VS 分析器无法确定可能不会 Dispose 的代码路径。 - InBetween
好的,我完全不怪他们。:-) 确保在源代码中忽略警告时,请使用“Justification”属性记录在该特定情况下忽略警告的原因。我还建议这样做。 - John Saunders

1

你的一次性模式看起来对我来说有点不太对。我认为你不应该在FooBase类中调用bar.Dispose。为了对象的安全处理和能够安全地多次调用Dispose,我建议采用以下方法。

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

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

           _disposed = true;
        }
     }
  }

关于这个错误,我认为这应该解决了静态分析警告。我已经在测试项目中按照你的代码实现,并启用了所有静态分析警告,没有出现任何警告的问题。
public class Bar : IDisposable
{
  private bool _disposed;
  public void Dispose()
  {
     Dispose( true );
     GC.SuppressFinalize( this );
  }

  protected virtual void Dispose( bool disposing )
  {
     if ( disposing )
     {
        if ( !_disposed )
        {
           _disposed = true;
        }
     }
  }
}

public abstract class FooBase : IDisposable
{
  public Bar Bar
  {
     get;
     set;
  }

  internal FooBase( Bar bar )
  {
     Bar = bar;
  }

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

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

           _disposed = true;
        }
     }
  }
}

public class FooA : FooBase
{
  public FooA( Bar bar )
     : base( bar )
  {
  }
}

public static class FooProvider
{
  public static FooA GetFooA()
  {
     Bar bar;
     using ( bar = new Bar() )
     {
        return new FooA( bar );
     }
  }
}

[TestClass]
public class UnitTest1
{
  [TestMethod]
  public void StaticAnalysisTest()
  {
     Assert.IsNotNull( FooProvider.GetFooA().Bar );
  }
}

我希望这对你有所帮助。

抱歉,我在发布代码时没有好好想一下。Dispose模式完全错了。我已经编辑过了,谢谢!代码实际上与您编写的相同。 - InBetween
请注意FooProvider中FooA GetFooA()的实现。一旦可处理静态分析警告的可处理模式得到更新,问题就会得到解决。单元测试代码显示,在方法退出后,Bar仍然可以访问且未被清空。 - Brian Dishaw
是的,你的代码应该消除警告,因为你正在向bar添加一个公共get属性。但这有两个主要问题:你正在“破坏”黑盒并将bar暴露给FooX对象的消费者,我们根本不想要这样。其次,我们不希望FooX消费者甚至知道Bar类的存在,这就是Provider类的全部意义。 - InBetween
关于您的第二条评论,使用语句也不是一个有效的选项。如果我这样做,实际上是将一个带有已释放 bar 的 FooX 对象交给了 FooProvider 消费者。 - InBetween
我不确定将Bar公开为属性是解决问题的方法。我只是这样做是为了在单元测试中显示它在FooA被销毁之前没有被销毁。如果你将其更改为私有字段并编写一个方法返回它,它应该以相同的方式运行。祝你好运! - Brian Dishaw

0

至少这个问题的一部分并不是虚假的阳性,即使它不一定是一个非常有用的问题检测。为了修复剩余的问题,您需要在bar赋值之后立即打开try块,而不是在之前。例如:

Bar bar = new Bar();
try
{
    ///...            
    return new FooA(bar);
}
catch
{
    bar.Dispose();
    throw;
}

很不幸,在你做出这个改变之后,你仍然会得到一个CA2000违规警告,这可能是一个误报。那是因为该规则没有检查您是否将bar放入新创建的FooA状态中。如果它正在进入FooA的状态中,您可以安全地为违规行为创建抑制。然而,如果它没有进入FooA的状态,您应该在finally子句中而不是catch子句中处理它。


barFooA的状态成员,所以我可以抑制警告。我不太确定的是,如您所述,bar的初始化是否应该在try-catch块内。如果Bar构造函数抛出异常,那么我实际上没有一个Bar对象bar,所以我无法真正处理它,对吧?我不知道bar处于什么状态,如果一开始bar代表任何东西的话。 - InBetween
如果 Bar 构造函数抛出异常,您将无法使用部分构造的实例。您的 bar 变量值将保持为 null。 - Nicole Calinoiu

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