IDisposable 项目的迭代器函数

Iterator function for IDisposable items

假设我想创建一个生成 IDisposable 项的迭代器函数。

IEnumerable<Disposable> GetItems()
{
    yield return new Disposable();
    yield return new Disposable();
}

这对于客户端代码来说似乎并不理想:

foreach (var item in source.GetItems())
{
    using (item)
    {
        // ...
    }
}

直觉上,using 来得太晚了。事情可能会四处移动。可能会不小心在 foreachusing 之间插入代码 。不理想。

我正在寻找更好的选择!

想到的一种方法是创建以下 API 而不是迭代器函数:

// Client
while (source.HasItem)
{
    using (var item = source.GetNextItem())
    {
        // ...
    }
}

// Source
private IEnumerator<Disposable> Enumerator { get; }
private bool? _hasItem;
bool HasItem
{
    get
    {
        if (this._hasItem == null) this._hasItem = this.Enumerator.MoveNext();
        return this._hasItem;
    }
}
Disposable GetNextItem()
{
    if (!this.HasItem) throw new IndexOutOfBoundsException();
    this._hasItem = null;
    return this.Enumerator.Current;
}

不过现在看来源码要变成IDisposable了!不然它怎么知道什么时候释放 Enumerator?这可能是一个令人不快的副作用。

我正在寻找一种在客户中感觉可靠的替代方案,但它可以防止来源也变成 IDisposable

编辑 - 澄清:我忘了提到我们需要的一些内容来自迭代器。具体来说,假设我们要返回 IDbCommand 个实例,即 IDisposable。在返回每个命令之前,我们需要用一些查询数据填充它,而这些查询数据又来自一个简单的迭代器方法。

如果我没理解错的话,下面的模式适合你

foreach (var item in source.GetItems())
{
    using (item)
    {
        // ...
    }
}

但潜在的问题是将一些代码放在 using 块之外。那么,为什么不将该逻辑包装在自定义扩展方法中:

public static class EnumerableExtennisons
{
    public static IEnumerable<T> WithUsing<T>(this IEnumerable<T> source)
        where T : IDisposable
    {
        foreach (var item in source)
        {
            using (item)
                yield return item;
        }
    }
}

通过这种方式,您可以确保在将项目返回给调用者之前*将项目包装在 using 块中,因此调用者无法插入代码 before/after。 C# 编译器生成的代码确保 item.DisposeIEnumerator<T>MoveNextDispose 方法中被调用(以防迭代提前结束)。

用法是在需要的地方附加 .WithUsing() 调用而不是 using 块:

foreach (var item in source.GetItems().WithUsing())
{
    // ...
}

迭代器函数本身可以负责放置一个 using(即在请求下一个项目时处理当前项目)。

IEnumerable<Disposable> GetItems()
{
    // Assumming CreateItems() is an iterator as well
    foreach (var item in this.CreateItems())
        using (item) yield return item;
}

因此,我们基本上将拥有一个仅流式处理的迭代器。 ToList()这样的方法变得毫无意义,因为一次只能使用一个项目。

不幸的是,这确实为错误创造了空间。

我们只能暴露一个IEnumerator<T>,它只支持MoveNext()Current

现在,底层的 private 迭代器函数可以流式传输,负责处理项目。不会向客户端引入无效操作 - 除非他们尝试存储借用的对象并稍后尝试使用它们,此时很明显对象已经被处置。

// Client
using (var itemEnumerator = source.GetItemEnumerator())
{
    while (itemEnumerator.MoveNext())
        var current = itemEnumerator.Current;
}

// Source
IEnumerator<Disposable> GetItemEnumerator() => this.StreamItems().GetEnumerator();
private IEnumerable<Disposable> StreamItems()
{
    while (this.ShouldCreateItem())
    {
        using (var item = this.CreateItem())
        {
            yield return item;
        }
    }
}