如何使用LINQ处理IDisposable序列?

3

如何最好地调用序列中元素的Dispose()方法?

假设有以下代码:

IEnumerable<string> locations = ...
var streams = locations.Select ( a => new FileStream ( a , FileMode.Open ) );
var notEmptyStreams = streams.Where ( a => a.Length > 0 );
//from this point on only `notEmptyStreams` will be used/visible
var firstBytes = notEmptyStreams.Select ( a => a.ReadByte () );
var average = firstBytes.Average ();

在保持代码简洁的同时,如何处理FileStream实例(一旦不再需要)?


澄清一下:这不是实际的代码片段,这些行是分布在一组类中的方法,FileStream类型也只是一个示例。


是否可以采用以下方式:

public static IEnumerable<TSource> Where<TSource> (
            this IEnumerable<TSource> source ,
            Func<TSource , bool> predicate
        )
        where TSource : IDisposable {
    foreach ( var item in source ) {
        if ( predicate ( item ) ) {
            yield return item;
        }
        else {
            item.Dispose ();
        }
    }
}

这是一个好主意吗?


或者说:当涉及到 IEnumerable<IDisposable> 的非常特定的场景时,你是否总是试图将其泛化处理?这是因为拥有它是不典型的情况吗?你是否从一开始就设计了它?如果是这样,那么你是如何设计的?


1
我不喜欢这个问题的地方在于,如果没有一个具体的场景来实现,所有答案中的努力可能完全是无用的。这会导致有趣的讨论,但可能远离解决真正的问题。 - eglasius
针对特定场景的专业解决方案很可能是微不足道的,正如迄今为止所展示的答案一样。我想知道是否有一种更通用的方法来处理IEnumerable<IDisposable>。在IDisposable周围创建一个单子是朝着这个方向迈出的一步,我认为。 - chase
我认为你的方法示例不是一个好主意。LINQ方法不应该对枚举值产生副作用,而你的方法有一个相当严重的副作用。 - zneak
6个回答

3

我建议你将streams变量转换为ArrayList,因为第二次枚举它(如果我没有错)会创建流的新副本。

var streams = locations.Select(a => new FileStream(a, FileMode.Open)).ToList();
// dispose right away of those you won't need
foreach (FileStream stream in streams.Where(a => a.Length == 0))
    stream.Dispose();

var notEmptyStreams = streams.Where(a => a.Length > 0);
// the rest of your code here

foreach (FileStream stream in notEmptyStreams)
    stream.Dispose();

编辑 对于这些限制,也许LINQ并不是最好的工具。也许你可以使用简单的foreach循环来解决问题?

var streams = locations.Select(a => new FileStream(a, FileMode.Open));
int count = 0;
int sum = 0;
foreach (FileStream stream in streams) using (stream)
{
    if (stream.Length == 0) continue;
    count++;
    sum += stream.ReadByte();
}
int average = sum / count;

+1:我认为你关于没有使用ToArray的临时副本是正确的:我之前没有注意到。 - Mark Byers
关于第二次迭代的观点很好。这正是我正在寻找一种模式的原因,即实例在使用后会被处理,因此在第二次迭代时,“旧”的实例将不再存在(或者至少不会占用资源)。 - chase
@chase 如果这是你的关注点,正如我所说,将序列转换为数组将防止选择器进一步评估,因此你不会得到每个流的多个副本。 - zneak
是的,但序列具有许多元素,每个元素都拥有多个句柄和大块内存。生成器是唯一的选择,数组或列表行不通。这就是为什么我宁愿早日处理它们而不是晚些处理。 - chase
@chase 那么也许LINQ不是你要寻找的工具。如果你想达到这种性能,就需要做出一些牺牲。那用一个简单的foreach怎么样? - zneak

3
我会写一个方法,比如说 AsDisposableCollection,它返回一个包装的 IEnumerable,同时也实现了 IDisposable,这样你就可以使用通常的 using 模式。这需要一些额外的工作(实现该方法),但你只需要做一次,然后就可以愉快地使用该方法(无论需要多少次):
using(var streams = locations.Select(a => new FileStream(a, FileMode.Open))
                             .AsDisposableCollection()) {
  // ...
} 

实现大致如下(这不是完整的实现,只是为了展示思路):
class DisposableCollection<T> : IDisposable, IEnumerable<T> 
                                where T : IDisposable {
  IEnumerable<T> en; // Wrapped enumerable
  List<T> garbage;   // To keep generated objects

  public DisposableCollection(IEnumerable<T> en) {
    this.en = en;
    this.garbage = new List<T>();
  }
  // Enumerates over all the elements and stores generated
  // elements in a list of garbage (to be disposed)
  public IEnumerator<T> GetEnumerator() { 
    foreach(var o in en) { 
      garbage.Add(o);
      yield return o;
    }
  }
  // Dispose all elements that were generated so far...
  public Dispose() {
    foreach(var o in garbage) o.Dispose();
  }
}

我一直在思考这个问题。但是假设一个方法返回DisposableCollection<>。这意味着你要么放弃过滤结果的能力(或者失去“可处理性”),要么失去LINQ的流畅性,或者不得不实现自己的.Where()等方法,并使它们也返回DisposableCollection<>。 - chase

2
一个简单的解决方案如下:
List<Stream> streams = locations
    .Select(a => new FileStream(a, FileMode.Open))
    .ToList();

try
{
    // Use the streams.
}
finally
{
    foreach (IDisposable stream in streams)
        stream.Dispose();
}

请注意,即使使用这种方法,如果其中一个FileStream构造函数在其他已经构造完成后失败,理论上仍然可能无法关闭流。为了解决这个问题,您需要更加小心地构建初始列表:
List<Stream> streams = new List<Stream>();
try
{
    foreach (string location in locations)
    {
        streams.Add(new FileStream(location, FileMode.Open));
    }

    // Use the streams.
}
finally { /* same as before */ }

这是一段英文文本,翻译如下:

代码量很大,也不像你想要的那样简洁,但如果你想确保所有流都被关闭,即使出现异常,那么你应该这样做。

如果你想要更像LINQ的东西,你可能需要阅读Marc Gravell的这篇文章:


这假设在代码中有一个特定的位置可以放置它。此外,它不会立即处理空流,因为一旦确定它们为空且不再需要,就会被处理。 - chase
我想说的是,如果从 "stream.Dispose()"(例如 NullRefException)抛出异常,这也会阻止流被关闭,不是吗? - Alxandr

1

使用https://lostechies.com/keithdahlby/2009/07/23/using-idisposables-with-linq/中的代码,您可以将查询转换为以下内容:

(
    from location in locations
    from stream in new FileStream(location, FileMode.Open).Use()
    where stream.Length > 0
    select stream.ReadByte()).Average()

您需要以下扩展方法:

public static IEnumerable<T> Use<T>(this T obj) where T : IDisposable
{
    try
    {
        yield return obj;
    }
    finally
    {
        if (obj != null)
            obj.Dispose();
    }
}

这将正确地处理您创建的所有流,无论它们是否为空。

1

描述

我想出了一个通用的解决方案 :)
对我来说,重要的一点是即使我没有迭代整个枚举,也要正确地处理所有内容,这就是我经常使用 FirstOrDefault 等方法的情况。

因此,我想出了一个自定义的枚举器来处理所有的处理。您只需要调用 AsDisposeableEnumerable 即可为您完成所有的魔法。

GetMy.Disposeables()
    .AsDisposeableEnumerable() // <-- all the magic is injected here
    .Skip(5)
    .where(i => i > 1024)
    .Select(i => new {myNumber = i})
    .FirstOrDefault()

请注意,这适用于有限枚举,无限枚举不适用。

代码

  1. 我的自定义IEnumerable

    public class DisposeableEnumerable<T> : IEnumerable<T> where T : System.IDisposable
    {
        private readonly IEnumerable<T> _enumerable;
    
        public DisposeableEnumerable(IEnumerable<T> enumerable)
        {
            _enumerable = enumerable;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return new DisposeableEnumerator<T>(_enumerable.GetEnumerator());
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    
  2. 我的自定义IEnumerator

    public class DisposeableEnumerator<T> : IEnumerator<T> where T : System.IDisposable
    {
        readonly List<T> toBeDisposed = new List<T>();
    
        private readonly IEnumerator<T> _enumerator;
    
        public DisposeableEnumerator(IEnumerator<T> enumerator)
        {
            _enumerator = enumerator;
        }
    
        public void Dispose()
        {
            // 处理剩余的可处理项
            while (_enumerator.MoveNext()) {
                T current = _enumerator.Current;
                current.Dispose();
            }
    
            // 处理提供的可处理项
            foreach (T disposeable in toBeDisposed) {
                disposeable.Dispose();
            }
    
            // 处理内部枚举器
            _enumerator.Dispose();
        }
    
        public bool MoveNext()
        {
            bool result = _enumerator.MoveNext();
    
            if (result) {
                toBeDisposed.Add(_enumerator.Current);
            }
    
            return result;
        }
    
        public void Reset()
        {
            _enumerator.Reset();
        }
    
        public T Current
        {
            get
            {
                return _enumerator.Current;
            }
        }
    
        object IEnumerator.Current
        {
            get { return Current; }
        }
    }
    
  3. 一个高端的扩展方法,使事情看起来更好

    public static class IDisposeableEnumerableExtensions
    {
        /// <summary>
        /// 将给定的可枚举项包装到DisposeableEnumerable中,以确保正确处理所有可处理项
        /// </summary>
        /// <typeparam name="T">IDisposeable类型</typeparam>
        /// <param name="enumerable">要确保处理元素的可枚举项</param>
        /// <returns></returns>
        public static DisposeableEnumerable<T> AsDisposeableEnumerable<T>(this IEnumerable<T> enumerable) where T : System.IDisposable
        {
            return new DisposeableEnumerable<T>(enumerable);
        }
    }
    

0

这里有一个简单的包装器,允许您使用 using 处理任何 IEnumerable(为了保留集合类型而不是将其转换为 IEnumerable,我们需要嵌套泛型参数类型 C#似乎不支持):

public static class DisposableEnumerableExtensions {
    public static DisposableEnumerable<T> AsDisposable<T>(this IEnumerable<T> enumerable) where T : IDisposable {
        return new DisposableEnumerable<T>(enumerable);
    }
}

public class DisposableEnumerable<T> : IDisposable where T : IDisposable {
    public IEnumerable<T> Enumerable { get; }

    public DisposableEnumerable(IEnumerable<T> enumerable) {
        this.Enumerable = enumerable;
    }

    public void Dispose() {
        foreach (var o in this.Enumerable) o.Dispose();
    }
}

使用方法:

using (var processes = System.Diagnostics.Process.GetProcesses().AsDisposable()) {
    foreach (var p in processes.Enumerable) {
        Console.Write(p.Id);
    }
}

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