【问题标题】:How do you deal with sequences of IDisposable using LINQ?您如何使用 LINQ 处理 IDisposable 序列?
【发布时间】:2015-11-12 22:35:11
【问题描述】:

在序列元素上调用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&lt;IDisposable&gt; 的非常具体的场景?是不是因为拥有它是一种不典型的情况?您是否首先围绕拥有它进行设计?如果有,怎么做?

【问题讨论】:

  • 我不喜欢这个问题的一点是,如果没有您试图实现的具体场景,答案中的所有努力都可能完全失败。这引发了一次有趣的讨论,但可能远未找到真正的解决方案。
  • 针对特定场景的专门解决方案很可能是微不足道的,正如迄今为止的答案所示。如果有更通用的方法来处理 IEnumerable,我很感兴趣。我认为,围绕 IDisposable 创建一个 monad 是朝着这个方向迈出的一步。
  • 我认为您的方法示例不是一个好主意。 LINQ 方法不应该对枚举值产生副作用,而您的方法有一个非常严重的副作用。

标签: c# linq ienumerable dispose idisposable


【解决方案1】:

我会写一个方法,比如说,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 为好吧。
【解决方案2】:

我建议您将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 的临时副本是正确的:我没有注意到。
  • 关于第二次迭代的好点。这正是为什么我正在寻找一种模式,在这种模式下,实例一旦被使用就会被丢弃,因此在第二次迭代时,“旧”实例将不再存在(或至少不再保留资源)。
  • @chase 如果这是您关心的问题,正如我所说,将序列转换为数组将阻止对选择器的进一步评估,因此您不会得到每个流的多个副本。跨度>
  • true,但是这个序列有很多元素,每个元素都有几个句柄和大内存块。生成器是唯一的选择,数组或列表不行。这就是为什么我宁愿早点处置它们。
  • @chase 那么也许 LINQ 不是您正在寻找的工具。如果您想获得这种性能,就需要做出一些牺牲。运行一个简单的 foreach 怎么样?
【解决方案3】:

一个简单的解决方案如下:

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 的这篇文章:

【讨论】:

  • 这假设代码中有一个特定的位置可以放置它。此外,一旦确定它们为空且不再需要,它也不会立即处理空流。
  • 我还想说,如果从“stream.Dispose()”抛出异常(例如 NullRefException),这也会阻止流被关闭,不是吗?
【解决方案4】:

使用来自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();
    }
}

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

【讨论】:

    【解决方案5】:

    说明

    我想出了一个通用的解决方案:)
    对我来说重要的一件事是一切都正确处理,即使我不迭代整个枚举,当我使用像 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()
          {
              // dispose the remaining disposeables
              while (_enumerator.MoveNext()) {
                  T current = _enumerator.Current;
                  current.Dispose();
              }
      
              // dispose the provided disposeables
              foreach (T disposeable in toBeDisposed) {
                  disposeable.Dispose();
              }
      
              // dispose the internal enumerator
              _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>
          /// Wraps the given IEnumarable into a DisposeableEnumerable which ensures that all the disposeables are disposed correctly
          /// </summary>
          /// <typeparam name="T">The IDisposeable type</typeparam>
          /// <param name="enumerable">The enumerable to ensure disposing the elements of</param>
          /// <returns></returns>
          public static DisposeableEnumerable<T> AsDisposeableEnumerable<T>(this IEnumerable<T> enumerable) where T : System.IDisposable
          {
              return new DisposeableEnumerable<T>(enumerable);
          }
      }
      

    【讨论】:

      【解决方案6】:

      这是一个简单的包装器,它允许您使用using 处理任何IEnumerable(为了保留集合类型而不是强制转换为IEnumerable,我们需要嵌套的泛型参数类型which C# does not seem to support):

      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);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-10-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-20
        相关资源
        最近更新 更多