实现 IDisposable/IDisposeAsync 作为抽象基类

问题描述 投票:0回答:1

此问题特定于 .NET 9 下的多线程应用程序。

我经常处理 COM Interop 场景,其中引用必须按特定顺序处理,而获取它们的顺序是不确定的。我还认为,在处理编程构造而不是业务逻辑的场景中,接口继承是比实现继承更好的方法。然而,这个问题与方法无关。

我使用

IDisposable
模式的具体、抽象实现作为顶级类的基础,需要对处理顺序进行精细控制。这是我第一次实现
IAsyncDispose
模式,并尝试遵守此处提供的文档。我仍然在思考这个问题,所以不确定问题标题是否合适。

internal abstract class Disposable:
    IDisposable,
    IAsyncDisposable
{
    private bool Disposed = false;

    private readonly List<SafeHandle?> SafeHandleList = [];
    private readonly Stack<SafeHandle?> SafeHandleStack = [];
    private readonly Queue<SafeHandle?> SafeHandleQueue = [];

    private readonly List<IDisposable?> DisposableList = [];
    private readonly Stack<IDisposable?> DisposableStack = [];
    private readonly Queue<IDisposable?> DisposableQueue = [];

    private readonly List<IAsyncDisposable?> AsyncDisposableList = [];
    private readonly Stack<IAsyncDisposable?> AsyncDisposableStack = [];
    private readonly Queue<IAsyncDisposable?> AsyncDisposableQueue = [];

    protected Disposable () { }

    ~Disposable () { this.Dispose(disposing: false); }

    public bool IsDisposed => this.Disposed;

    protected T AddDisposable<T> (T disposable) where T : IDisposable
    { this.ThrowDisposedException(); this.DisposableList.Add(disposable); return disposable; }

    protected T PushDisposable<T> (T disposable) where T : IDisposable
    { this.ThrowDisposedException(); this.DisposableStack.Push(disposable); return disposable; }

    protected T EnqueueDisposable<T> (T disposable) where T : IDisposable
    { this.ThrowDisposedException(); this.DisposableQueue.Enqueue(disposable); return disposable; }

    protected T AddAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
    { this.ThrowDisposedException(); this.AsyncDisposableList.Add(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableList.Add(disposable); } return asyncDisposable; }

    protected T PushAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
    { this.ThrowDisposedException(); this.AsyncDisposableStack.Push(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableStack.Push(disposable); } return asyncDisposable; }

    protected T EnqueueAsyncDisposable<T> (T asyncDisposable) where T : IAsyncDisposable
    { this.ThrowDisposedException(); this.AsyncDisposableQueue.Enqueue(asyncDisposable); if (asyncDisposable is IDisposable disposable) { this.DisposableQueue.Enqueue(disposable); } return asyncDisposable; }

    protected T AddSafeHandle<T> (T safeHandle) where T : SafeHandle
    { this.ThrowDisposedException(); this.SafeHandleList.Add(safeHandle); return safeHandle; }

    protected T PushSafeHandle<T> (T disposable) where T : SafeHandle
    { this.ThrowDisposedException(); this.SafeHandleStack.Push(disposable); return disposable; }

    protected T EnqueueSafeHandle<T> (T disposable) where T : SafeHandle
    { this.ThrowDisposedException(); this.SafeHandleQueue.Enqueue(disposable); return disposable; }

    public void ThrowDisposedException ()
    {
        if (this.Disposed)
        {
            var type = this.GetType();

            throw new ObjectDisposedException(type.FullName, $@"Attempt to access a disposed object: [{type.FullName}].");
        }
    }

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

    protected virtual void Dispose (bool disposing)
    {
        if (!this.Disposed)
        {
            if (disposing)
            {
                // Dispose objects implementing [IDisposable] and [SafeHandle].

                while (this.DisposableList.Count > 0) { try { this.DisposableList [0]?.Dispose(); } catch { } this.DisposableList.RemoveAt(0); }
                while (this.DisposableStack.Count > 0) { try { this.DisposableStack.Pop()?.Dispose(); } catch { } }
                while (this.DisposableQueue.Count > 0) { try { this.DisposableQueue.Dequeue()?.Dispose(); } catch { } }

                while (this.SafeHandleList.Count > 0) { try { this.SafeHandleList [0]?.Dispose(); } catch { } this.SafeHandleList.RemoveAt(0); }
                while (this.SafeHandleStack.Count > 0) { try { this.SafeHandleStack.Pop()?.Dispose(); } catch { } }
                while (this.SafeHandleQueue.Count > 0) { try { this.SafeHandleQueue.Dequeue()?.Dispose(); } catch { } }

                // This approach is meant to help both async and non-async consumption scenarios.
                // Any objects implementing [IAsyncDisposable] as well as [IDisposable] have already been dealt with at this point.
                // https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-disposeasync#implement-both-dispose-and-async-dispose-patterns.
                // It is up to the comsuming code to ensure that [DisposeAsync] is called, if needed. Example: [using (var ad = new AsyncDisposable())] vs. [await using (var ad =  = new AsyncDisposable())].
            }

            // Dispose unmanaged resources (excluding [SafeHandle] objects).
            // Free unmanaged resources (unmanaged objects), override finalizer, and set large fields to null.

            this.Disposed = true;
        }
    }

    public async ValueTask DisposeAsync ()
    {
        // Perform asynchronous cleanup.
        await this.DisposeAsyncCore()
            .ConfigureAwait(false);

        // Dispose of unmanaged resources.
        this.Dispose(false);

        // Suppress finalization.
        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore ()
    {
        if (!this.Disposed)
        {
            while (this.AsyncDisposableList.Count > 0)
            {
                var asyncDisposable = this.AsyncDisposableList [0];

                if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
                this.AsyncDisposableList.RemoveAt(0);
            }

            while (this.AsyncDisposableStack.Count > 0)
            {
                var asyncDisposable = this.AsyncDisposableStack.Pop();
                if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
            }

            while (this.AsyncDisposableQueue.Count > 0)
            {
                var asyncDisposable = this.AsyncDisposableQueue.Dequeue();
                if (asyncDisposable is not null) { try { await asyncDisposable.DisposeAsync().ConfigureAwait(false); } catch { } }
            }

            // TODO: We want to ensure that objects implementing only [IDisposable] are handled as well.
            // Although there are not circular references between the dispose mthods, calling [this.Dispose()] here directly smells funny.
            this.Dispose();

            this.Disposed = true;
        }
    }
}

虽然我尝试遵守最佳实践,但我不确定以下几点:

  • Dispose
    内部呼叫
    DisposeAsyncCore
    。我有些不舒服。或许我还没有想清楚这一点。我应该调用虚拟实现吗
    Dispose(disposing: true|false???)
  • 如果使用代码调用 [
    using (var ad = new AsyncDisposable())
    ] 与 [
    await using (var ad = new AsyncDisposable())
    ],是否有任何问题?
  • 在承保范围方面我是否遗漏了任何明显的内容? [
    SafeHandle
    ] 物品应该扔到哪里?等等
  • 欢迎对此实施作为一种模式提出任何一般性意见。

您的目标是允许继承的类能够根据需要添加一次性对象,而不必覆盖/加重它们自己的

Dispose(bool)/DisposeAsyncCore
覆盖。

如有任何建议,我们将不胜感激。

这里有一个用法示例以供澄清:

public sealed class SampleTask:
    Disposable
{
    private readonly MemoryStream Stream1;
    private readonly Excel.Application Application;

    public SampleTask ()
    {
        // Add child objects in any desired order.
        this.Application = this.AddDisposable(new Excel.Application());
        this.Stream1 = this.PushAsyncDisposable(new MemoryStream());
    }

    // This class needs to have control over the lifetime of some
    // exposed objects irrespective of their external reference count.
    public Image GetImage () => this.AddDisposable(new Bitmap(10, 10));
    public Excel.Worksheet GetExcelWorksheet () => this.AddDisposable(this.Application.Workbooks [0].Sheets [0]);

    protected override void Dispose (bool disposing) => base.Dispose(disposing);
    protected override ValueTask DisposeAsyncCore () => base.DisposeAsyncCore();
}
c# .net asynchronous dispose using
1个回答
0
投票

您的代码,就像许多处理终结的代码(实际上是 C# 的“析构函数”的设计)一样,似乎因对终结实际工作原理的一些误解而受到影响。

与流行的看法相反,当对象

被垃圾收集时,对象的 
Finalize 方法不会运行,而是当 本来会被垃圾收集时运行,但由于存在注册的终结器某处。 已注册终结器的存在将阻止一个对象或它持有强引用的任何对象“实际上”被垃圾收集,直到终结器被取消注册(这将由于触发它而发生),并且只要对对象的引用存在于宇宙中的任何地方,就不可能确定代码不会再次尝试使用对象。 如果希望拥有在对象“实际上”被垃圾收集时执行的代码,则应该有一个面向公众的“shell”对象,该对象构造并保存对两个或三个私有对象的引用。 其中之一应该封装主要对象功能并处理任何订阅的事件或通知,其中之一应该是“金丝雀”对象,当其所有者被垃圾收集时,该对象将发出声音。 第三个对象将保存足够的信息来执行清理,但仅当第一个对象保存对外部对象的引用时才需要。 Canary 对象应该对其所有者(shell 对象)持有一个“长弱引用”(一个 WeakReference

,可选的布尔构造函数参数设置为 true),以及对负责清理的对象及其 Finalize 方法的普通引用应该检查

WeakReference 是否仍然存在并重新注册自身以进行终结(如果 WeakReference

 仍然存在)或调用主功能对象的清理方法。  请注意,当调用 cleanup 方法时,主对象可能仍然是事件订阅之类的目标,但代码可以确定宇宙中的任何地方都不会再存在对外部对象的引用。
自从我在 .NET 中完成大量编码以来,许多类型确实不需要对终结进行任何操作,但如果需要终结,我建议使用多对象模式并小心确保没有引用循环会将 Canary 对象或清理对象连接回 shell 对象,并且 shell 对象之外不存在对 Canary 对象的强引用。  当终结确实触发时,应该设法最小化无法被垃圾收集的对象的范围。
	

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.