当一个IDisposable对象被传入父IDisposable对象时,会发生什么行为?

7
昨天,在我们的代码库上运行 Visual Studio 代码分析后,以下代码被标记为问题:
using (var stringReader = new StringReader(someString))
{
    using (var reader = XmlReader.Create(stringReader)) {
    // Code
    }
}

返回结果:

返回的警告信息如下:

警告 CA2202:“(方法名)”中的对象“stringReader”可以被多次处理。为了避免生成 System.ObjectDisposedException,您不应在一个对象上调用 Dispose 大于一次。

在搜索 stack overflow 后,我总结出了一个普遍的理解:如果我创建了一个包含 IDisposable 成员的自定义类,则该类本身应该实现 IDisposable,并调用成员的 dispose() 方法。

我的两个问题是:

  • 在所有情况下,当对象 X 以参数形式接收可处理对象 Y 的引用来创建时,是否正确假设对象 X 将拥有 Y,并从那时起,调用 X.dispose() 将始终导致调用 Y.dispose()?
  • 这是一段旧代码,并且从未报告过和警告信息描述相同的异常(据我所知)。如果假定上述点,为什么双重 using 块不会导致调用 stringReader.dispose() 两次,从而抛出异常呢?

你应该期望封装类型的实现者已经记录下来了关于你交给它的可处理对象的行为。 - Damien_The_Unbeliever
3个回答

2
  • 不能假设另一个对象在处理自身时会调用Dispose()。拥有可释放对象引用的对象甚至可能不使用可释放资源。
  • 这个警告有点奇怪。请看这里看一些对这个警告的抱怨。你应该设计你的类,使得多次调用Dispose()是安全的。

顺便说一下,MSDN上说:

方法实现包含可能导致多次对IDisposable.Dispose或等效的Dispose(某些类型上的Close()方法)进行调用的代码路径,同一对象上。

因此,调用Close()方法的路径也会生成这个警告,这就是为什么在您的情况下会看到这个警告。


2
“假设对象X将拥有Y,并且从那时起,调用X.dispose()将始终导致调用Y.dispose()”,这种假设是正确的吗?
不,这种假设永远都不能成立。我们来看一个具体的例子:XmlReader.Create(Stream)
在参考源代码中,我发现Dispose方法调用了Close方法,这很明显。然后请注意这段代码
public override void Close() {
    Close( closeInput );
}

因此,备份流是否关闭和释放取决于设置closeInput的值,您可以通过XmlReaderSettings.CloseInput设置进行设置。
所以,答案是肯定不能确定它已被处理。您应该始终确保自己处理它。

1
在创建过程中,如果对象X以IDisposable对象Y作为参数获取引用,是否正确假设对象X将拥有Y,并且从那时起,调用X.dispose()将始终导致调用Y.dispose()?
我认为不是,并且我将尝试解释原因。
有一种称为IDisposablePattern的模式,大致如下:
public class SimpleClass : IDisposable
{
    // managed resources SqlConnection implements IDisposable as well.
    private SqlConnection _connection;
    private bool _disposed;

    // implementing IDisposable
    public void Dispose()
    {
        // Here in original Dispose method we call protected method with parameter true,
        // saying that this object is being disposed.
        this.Dispose(true);

        // Then we "tell" garbage collector to suppress finalizer for this object because we are releasing
        // its memory and doesnt need to be finalized. Calling finalizer(destructor) of a given type is expensive
        // and tweaks like this help us improve performance of the application.
        GC.SuppressFinalize(this);
    }

    // Following the best practices we should create another method in the class 
    // with parameter saying whether or not the object is being disposed.
    // Its really important that this method DOES NOT throw exceptions thus allowing to be called multiple times 
    protected virtual void Dispose(bool disposing)
    {
        // another thing we may add is flag that tells us if object is disposed already
        // and use it here
        if (_disposed) { return; }
        if (_connection != null)
        {
            _connection.Dispose();
            _connection = null;
        }
        _disposed = true;

        // call base Dispose(flag) method if we are using hierarchy.
    }
}

请注意,当您的类使用像这样的非托管资源时,可以将其扩展到新的级别:
    public class SimpleClass2: IDisposable
{
    // managed resources
    private SqlConnection _connection;
    private bool _disposed;

    // unmanaged resources
    private IntPtr _unmanagedResources;

    // simple method for the demo
    public string GetDate()
    {
        // One good practice that .NET Framework implies is that when object is being disposed
        // trying to work with its resources should throw ObjectDisposedException so..
        if(_disposed) { throw new ObjectDisposedException(this.GetType().Name);}

        if (_connection == null)
        {
            _connection = new SqlConnection("Server=.\\SQLEXPRESS;Database=master;Integrated Security=SSPI;App=IDisposablePattern");
            _connection.Open();
        }
        // allocation of unmanaged resources for the sake of demo.
        if (_unmanagedResources == IntPtr.Zero)
        {
            _unmanagedResources = Marshal.AllocHGlobal(100 * 1024 * 1024);
        }

        using (var command = _connection.CreateCommand())
        {
            command.CommandText = "SELECT getdate()";
            return command.ExecuteScalar().ToString();
        }
    }


    public void Dispose()
    {
        // Here in original Dispose method we call protected method with parameter true,
        // saying that this object is being disposed.
        this.Dispose(true);

        // Then we "tell" garbage collector to suppress finalizer for this object because we are releasing
        // its memory and doesnt need to be finalized. Calling finalizer(destructor) of a given type is expensive
        // and tweaks like this help us improve performance of the application.

        // This is only when your class doesnt have unmanaged resources!!!
        // Since this is just made to be a demo I will leave it there, but this contradicts with our defined finalizer.
        GC.SuppressFinalize(this);
    }

    // Following the best practices we should create another method in the class 
    // with parameter saying wether or not the object is being disposed.
    // Its really important that this method DOES NOT throw exceptions thus allowing to be called multiple times 
    protected virtual void Dispose(bool disposing)
    {
        // another thing we may add is flag that tells us if object is disposed already
        // and use it here
        if (_disposed) { return; }
        // Thus Dispose method CAN NOT release UNMANAGED resources such as IntPtr structure,
        // flag is also helping us know whether we are disposing managed or unmanaged resources
        if (disposing)
        {
            if (_connection != null)
            {
                _connection.Dispose();
                _connection = null;
            }
            _disposed = true;
        }
        // Why do we need to do that?
        // If consumer of this class forgets to call its Dispose method ( simply by not using the object in "using" statement
        // Nevertheless garbage collector will fire eventually and it will invoke Dispose method whats the problem with that is if we didn't 
        // have the following code unmanaged resources wouldnt be disposed , because as we know GC cant release unmanaged code.
        // So thats why we need destructor(finalizer).
        if (_unmanagedResources != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_unmanagedResources);
            _unmanagedResources = IntPtr.Zero;;
        }
        // call base Dispose(flag) method if we are using hierarchy.
    }

    ~DatabaseStateImpr()
    {
        // At this point GC called our finalizer method , meaning 
        // that we don't know what state our managed resources are (collected or not) because
        // our consumer may not used our object properly(not in using statement) so thats why
        // we skip unmanaged resources as they may have been finalized themselves and we cant guarantee that we can
        // access them - Remember? No exceptions in Dispose methods.
        Dispose(false);
    }
}

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