【问题标题】:Caching IEnumerable缓存 IEnumerable
【发布时间】:2010-12-04 22:55:27
【问题描述】:
public IEnumerable<ModuleData> ListModules()
{
    foreach (XElement m in Source.Descendants("Module"))
    {
        yield return new ModuleData(m.Element("ModuleID").Value);
    }
}

最初,上面的代码很棒,因为如果不需要,就不需要评估整个集合。

但是,一旦所有模块都被枚举一次,在没有变化的情况下重复查询 XDocument 变得更加昂贵。

因此,作为性能改进:

public IEnumerable<ModuleData> ListModules()
{
    if (Modules == null)
    {
        Modules = new List<ModuleData>();
        foreach (XElement m in Source.Descendants("Module"))
        {
            Modules.Add(new ModuleData(m.Element("ModuleID").Value, 1, 1));
        }
    }
    return Modules;
}

如果我重复使用整个列表,这很好,但其他情况就不那么好了。

是否有一个中间地带,我可以在整个列表被迭代之前产生 return,然后缓存它并将缓存提供给后续请求?

【问题讨论】:

  • 我得到了什么。错误的?您的代码似乎完全符合您的要求...
  • 第二个代码块将始终迭代整个可枚举,即使它可能不需要这样做。

标签: c# return ienumerable yield


【解决方案1】:

您可以查看Saving the State of Enumerators,它描述了如何创建惰性列表(它会缓存一次迭代的项目)。

【讨论】:

  • 非常酷!感谢您提供的链接,这完全解决了我在从磁盘读取查询时遇到的类似问题。
  • 对于后代,您能否在您的答案中包含您认为有用的链接的相关部分?这样,如果链接断开、更改等,您的答案不会变得无用。非常感谢。
  • 链接已损坏,如果只有 SO 有一条针对链接仅答案的规则...
  • Wes Dyer 的帖子仍然可以在web.archive.org/web/20190120224839/https://… 找到,但有趣的内容应该复制到答案中。
  • 请注意,其他答案提供了更高性能的解决方案,因为本文中提出的解决方案是递归的,并为枚举的每个元素分配一个对象。
【解决方案2】:

查看 Reactive Extensions for .NET 库 (Rx) 中的 MemoizeAll()。由于它是惰性评估的,您可以在构建过程中安全地设置它,然后从 ListModules() 返回 Modules

Modules = Source.
    Descendants("Module").
    Select(m => new ModuleData(m.Element("ModuleID").Value, 1, 1)).
    MemoizeAll();

MemoizeAll()(以及其他一些不太明显的 Rx 扩展)here 有很好的解释。

【讨论】:

  • 这个很好,我喜欢使用Rx。我仍在努力寻找时间和借口来更彻底地玩弄它。
【解决方案3】:

我喜欢@tsemer 的回答。但是我想提出我的解决方案,这与FP无关。这是一种天真的方法,但它产生的分配要少得多。而且它不是线程安全的。

public class CachedEnumerable<T> : IEnumerable<T>, IDisposable
{
    IEnumerator<T> _enumerator;
    readonly List<T> _cache = new List<T>();

    public CachedEnumerable(IEnumerable<T> enumerable) 
        : this(enumerable.GetEnumerator())
    {
    }

    public CachedEnumerable(IEnumerator<T> enumerator)
    {
        _enumerator = enumerator;
    }

    public IEnumerator<T> GetEnumerator()
    {
        // The index of the current item in the cache.
        int index = 0;

        // Enumerate the _cache first
        for (; index < _cache.Count; index++)
        {
            yield return _cache[index];
        }

        // Continue enumeration of the original _enumerator, 
        // until it is finished. 
        // This adds items to the cache and increment 
        for (; _enumerator != null && _enumerator.MoveNext(); index++)
        {
            var current = _enumerator.Current;
            _cache.Add(current);
            yield return current;
        }

        if (_enumerator != null)
        {
            _enumerator.Dispose();
            _enumerator = null;
        }

        // Some other users of the same instance of CachedEnumerable
        // can add more items to the cache, 
        // so we need to enumerate them as well
        for (; index < _cache.Count; index++)
        {
            yield return _cache[index];
        }
    }

    public void Dispose()
    {
        if (_enumerator != null)
        {
            _enumerator.Dispose();
            _enumerator = null;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

这就是@tsemer 答案中的矩阵测试的工作方式:

var ints = new [] { 1, 2, 3, 4, 5 };
var cachedEnumerable = new CachedEnumerable<int>(ints); 
foreach (var x in cachedEnumerable)
{
    foreach (var y in cachedEnumerable)
    {
        //Do something
    }
}
  1. 外层循环(x)首先跳过for,因为_cache是空的;
  2. x_enumerator 获取一项到_cache
  3. x 在第二个 for 循环之前暂停;
  4. 内部循环 (y) 从 _cache 中枚举一个元素;
  5. y 获取从_enumerator_cache 的所有元素;
  6. y 跳过第三个for 循环,因为它的index 变量等于5
  7. x 恢复,其 index 等于 1。它跳过第二个for 循环,因为_enumerator 已完成;
  8. x 使用第三个for 循环从_cache 中枚举一个元素;
  9. x 在第三个 for 之前暂停;
  10. y 使用第一个 for 循环从 _cache 中枚举 5 个元素;
  11. y 跳过第二个 for 循环,因为 _enumerator 已完成;
  12. y 跳过第三个for 循环,因为y 中的index 等于5
  13. x 恢复,递增 index。它使用第三个for 循环从_cache 中获取一个元素。
  14. x 暂停。
  15. 如果xindex变量小于5则转到10;
  16. 结束。

【讨论】:

  • 又好又干净,我也喜欢这个解决方案在实例化时不枚举第一项
  • 看起来简洁明了。请您解释一下为什么需要第三个for 块?
  • @djskinner 我添加了一些信息
  • 您的代码编辑无法编译,您应该从 _enumerator 中删除只读。作为旁注,处理代码虽然有用,但它是我试图避免的样板代码的一部分。此外,现在隐含地建议消费者在 using 指令中使用此类(或以其他方式手动处理),这会增加使用复杂性。
  • @tsemer 感谢您的更正。当您在文本框中编写代码时会发生这种情况:) 我知道,关于 usings 等等。这是故意的,因为否则资源可能会泄漏(我们正在为 havy 资源做这个可枚举的缓存,不是吗? )。因此,为我打开一个数据库连接或一个巨大的 XML 文件比“使用”关键字开销要大得多。
【解决方案4】:

我已经看到了一些实现,有些较旧并且没有利用最新的 .Net 类,有些过于复杂,无法满足我的需求。我最终得到了我能收集到的最简洁和声明性的代码,这些代码加起来是一个包含大约 15 行(实际)代码的类。它似乎很符合 OP 的需求:

编辑:第二次修订,更好地支持空枚举

/// <summary>
/// A <see cref="IEnumerable{T}"/> that caches every item upon first enumeration.
/// </summary>
/// <seealso cref="http://blogs.msdn.com/b/matt/archive/2008/03/14/digging-deeper-into-lazy-and-functional-c.aspx"/>
/// <seealso cref="http://blogs.msdn.com/b/wesdyer/archive/2007/02/13/the-virtues-of-laziness.aspx"/>
public class CachedEnumerable<T> : IEnumerable<T> {
  private readonly bool _hasItem; // Needed so an empty enumerable will not return null but an actual empty enumerable.
  private readonly T _item;
  private readonly Lazy<CachedEnumerable<T>> _nextItems;

  /// <summary>
  /// Initialises a new instance of <see cref="CachedEnumerable{T}"/> using <paramref name="item"/> as the current item
  /// and <paramref name="nextItems"/> as a value factory for the <see cref="CachedEnumerable{T}"/> containing the next items.
  /// </summary>
  protected internal CachedEnumerable(T item, Func<CachedEnumerable<T>> nextItems) {
    _hasItem = true;
    _item = item;
    _nextItems = new Lazy<CachedEnumerable<T>>(nextItems);
  }

  /// <summary>
  /// Initialises a new instance of <see cref="CachedEnumerable{T}"/> with no current item and no next items.
  /// </summary>
  protected internal CachedEnumerable() {
    _hasItem = false;
  }

  /// <summary>
  /// Instantiates and returns a <see cref="CachedEnumerable{T}"/> for a given <paramref name="enumerable"/>.
  /// Notice: The first item is always iterated through.
  /// </summary>
  public static CachedEnumerable<T> Create(IEnumerable<T> enumerable) {
    return Create(enumerable.GetEnumerator());
  }

  /// <summary>
  /// Instantiates and returns a <see cref="CachedEnumerable{T}"/> for a given <paramref name="enumerator"/>.
  /// Notice: The first item is always iterated through.
  /// </summary>
  private static CachedEnumerable<T> Create(IEnumerator<T> enumerator) {
    return enumerator.MoveNext() ? new CachedEnumerable<T>(enumerator.Current, () => Create(enumerator)) : new CachedEnumerable<T>();
  }

  /// <summary>
  /// Returns an enumerator that iterates through the collection.
  /// </summary>
  public IEnumerator<T> GetEnumerator() {
    if (_hasItem) {
      yield return _item;

      var nextItems = _nextItems.Value;
      if (nextItems != null) {
        foreach (var nextItem in nextItems) {
          yield return nextItem;
        }
      }
    }
  }

  /// <summary>
  /// Returns an enumerator that iterates through a collection.
  /// </summary>
  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator();
  }
}

一个有用的扩展方法可能是:

public static class IEnumerableExtensions {
  /// <summary>
  /// Instantiates and returns a <see cref="CachedEnumerable{T}"/> for a given <paramref name="enumerable"/>.
  /// Notice: The first item is always iterated through.
  /// </summary>
  public static CachedEnumerable<T> ToCachedEnumerable<T>(this IEnumerable<T> enumerable) {
    return CachedEnumerable<T>.Create(enumerable);
  }
}

对于你们中间的单元测试人员:(如果您不使用 resharper,只需取出 [SuppressMessage] 属性)

/// <summary>
/// Tests the <see cref="CachedEnumerable{T}"/> class.
/// </summary>
[TestFixture]
public class CachedEnumerableTest {
  private int _count;

  /// <remarks>
  /// This test case is only here to emphasise the problem with <see cref="IEnumerable{T}"/> which <see cref="CachedEnumerable{T}"/> attempts to solve.
  /// </remarks>
  [Test]
  [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
  [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed")]
  public void MultipleEnumerationAreNotCachedForOriginalIEnumerable() {
    _count = 0;

    var enumerable = Enumerable.Range(1, 40).Select(IncrementCount);

    enumerable.Take(3).ToArray();
    enumerable.Take(10).ToArray();
    enumerable.Take(4).ToArray();

    Assert.AreEqual(17, _count);
  }

  /// <remarks>
  /// This test case is only here to emphasise the problem with <see cref="IList{T}"/> which <see cref="CachedEnumerable{T}"/> attempts to solve.
  /// </remarks>
  [Test]
  [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
  [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed")]
  public void EntireListIsEnumeratedForOriginalListOrArray() {
    _count = 0;
    Enumerable.Range(1, 40).Select(IncrementCount).ToList();
    Assert.AreEqual(40, _count);

    _count = 0;
    Enumerable.Range(1, 40).Select(IncrementCount).ToArray();
    Assert.AreEqual(40, _count);
  }

  [Test]
  [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed")]
  public void MultipleEnumerationsAreCached() {
    _count = 0;

    var cachedEnumerable = Enumerable.Range(1, 40).Select(IncrementCount).ToCachedEnumerable();

    cachedEnumerable.Take(3).ToArray();
    cachedEnumerable.Take(10).ToArray();
    cachedEnumerable.Take(4).ToArray();

    Assert.AreEqual(10, _count);
  }

  [Test]
  public void FreshCachedEnumerableDoesNotEnumerateExceptFirstItem() {
    _count = 0;

    Enumerable.Range(1, 40).Select(IncrementCount).ToCachedEnumerable();

    Assert.AreEqual(1, _count);
  }

  /// <remarks>
  /// Based on Jon Skeet's test mentioned here: http://www.siepman.nl/blog/post/2013/10/09/LazyList-A-better-LINQ-result-cache-than-List.aspx
  /// </remarks>
  [Test]
  [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")]
  public void MatrixEnumerationIteratesAsExpectedWhileStillKeepingEnumeratedValuesCached() {
    _count = 0;

    var cachedEnumerable = Enumerable.Range(1, 5).Select(IncrementCount).ToCachedEnumerable();

    var matrixCount = 0;

    foreach (var x in cachedEnumerable) {
      foreach (var y in cachedEnumerable) {
        matrixCount++;
      }
    }

    Assert.AreEqual(5, _count);
    Assert.AreEqual(25, matrixCount);
  }

  [Test]
  public void OrderingCachedEnumerableWorksAsExpectedWhileStillKeepingEnumeratedValuesCached() {
    _count = 0;

    var cachedEnumerable = Enumerable.Range(1, 5).Select(IncrementCount).ToCachedEnumerable();

    var orderedEnumerated = cachedEnumerable.OrderBy(x => x);
    var orderedEnumeratedArray = orderedEnumerated.ToArray(); // Enumerated first time in ascending order.
    Assert.AreEqual(5, _count);

    for (int i = 0; i < orderedEnumeratedArray.Length; i++) {
      Assert.AreEqual(i + 1, orderedEnumeratedArray[i]);
    }

    var reorderedEnumeratedArray = orderedEnumerated.OrderByDescending(x => x).ToArray(); // Enumerated second time in descending order.
    Assert.AreEqual(5, _count);

    for (int i = 0; i < reorderedEnumeratedArray.Length; i++) {
      Assert.AreEqual(5 - i, reorderedEnumeratedArray[i]);
    }
  }

  private int IncrementCount(int value) {
    _count++;
    return value;
  }
}

【讨论】:

    【解决方案5】:

    我非常喜欢 hazzik 的回答...总是好的和简单的方式。 但是 GetEnumerator 中有一个错误

    它有点意识到存在问题,这就是为什么在第二个枚举数循环之后有一个奇怪的第三个循环......但它并不那么简单。触发需要第三个循环的问题是一般性的......所以它需要递归。

    虽然答案看起来更简单。

        public IEnumerator<T> GetEnumerator()
        {
            int index = 0;
    
            while (true)
            {
                if (index < _cache.Count)
                {
                    yield return _cache[index];
                    index = index + 1;
                }
                else
                {
                    if (_enumerator.MoveNext())
                    {
                        _cache.Add(_enumerator.Current);
                    }
                    else
                    {
                        yield break;
                    }
                }
            }
        }
    

    是的,你可以通过产生电流来提高效率……但我会接受微秒的打击……每个元素只会发生一次。

    它不是线程安全的......但谁在乎呢。

    【讨论】:

    【解决方案6】:

    简单总结一下:

    • this answer 中提出了一个解决方案,其中包含一个易于使用和单元测试的扩展方法。但是,由于它使用递归,因此由于分配较少,因此性能可能会比其他非递归解决方案更差。
    • this answer 中提供了一个非递归解决方案,包括一些代码来说明枚举对象被枚举两次的情况。但是,在这种情况下,它可能不会保持原始枚举的顺序,并且不会扩展到两个以上的并发枚举。
    • this answer 中,枚举器方法被重写,以泛化解决多个并发枚举情况的解决方案,同时保留原始可枚举的顺序。

    结合所有答案的代码,我们得到以下类。请注意,此代码不是线程安全的,这意味着并发枚举仅在同一个线程中是安全的。

    public class CachedEnumerable<T> : IEnumerable<T>, IDisposable
    {
        private readonly IEnumerator<T> enumerator;
        private readonly List<T> cache = new List<T>();
    
        public CachedEnumerable(IEnumerable<T> enumerable) : this(enumerable.GetEnumerator()) { }
    
        public CachedEnumerable(IEnumerator<T> enumerator)
            => this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator));
    
        public IEnumerator<T> GetEnumerator()
        {
            int index = 0;
            while (true) {
                if (index < cache.Count) {
                    yield return cache[index];
                    index++;
                }
                else if (enumerator.MoveNext())
                    cache.Add(enumerator.Current);
                else
                    yield break;
            }
        }
    
        public void Dispose() => enumerator.Dispose();
    
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    

    采用静态扩展方式,方便使用:

    public static class EnumerableUtils
    {
        public static CachedEnumerable<T> ToCachedEnumerable<T>(this IEnumerable<T> enumerable) 
            => new CachedEnumerable<T>(enumerable);
    }
    

    以及相应的单元测试:

    public class CachedEnumerableTest
    {
        private int _count;
    
        [Test]
        public void MultipleEnumerationAreNotCachedForOriginalIEnumerable()
        {
            _count = 0;
    
            var enumerable = Enumerable.Range(1, 40).Select(incrementCount);
    
            enumerable.Take(3).ToArray();
            enumerable.Take(10).ToArray();
            enumerable.Take(4).ToArray();
    
            Assert.AreEqual(17, _count);
        }
    
        [Test]
        public void EntireListIsEnumeratedForOriginalListOrArray()
        {
            _count = 0;
            Enumerable.Range(1, 40).Select(incrementCount).ToList();
            Assert.AreEqual(40, _count);
    
            _count = 0;
            Enumerable.Range(1, 40).Select(incrementCount).ToArray();
            Assert.AreEqual(40, _count);
        }
    
        [Test]
        public void MultipleEnumerationsAreCached()
        {
            _count = 0;
    
            var cachedEnumerable = Enumerable.Range(1, 40).Select(incrementCount).ToCachedEnumerable();
    
            cachedEnumerable.Take(3).ToArray();
            cachedEnumerable.Take(10).ToArray();
            cachedEnumerable.Take(4).ToArray();
    
            Assert.AreEqual(10, _count);
        }
    
        [Test]
        public void FreshCachedEnumerableDoesNotEnumerateExceptFirstItem()
        {
            _count = 0;
    
            Enumerable.Range(1, 40).Select(incrementCount).ToCachedEnumerable();
    
            Assert.That(_count <= 1);
        }
    
        [Test]
        public void MatrixEnumerationIteratesAsExpectedWhileStillKeepingEnumeratedValuesCached()
        {
            _count = 0;
    
            var cachedEnumerable = Enumerable.Range(1, 5).Select(incrementCount).ToCachedEnumerable();
    
            var matrixCount = 0;
    
            foreach (var x in cachedEnumerable) {
                foreach (var y in cachedEnumerable) {
                    matrixCount++;
                }
            }
    
            Assert.AreEqual(5, _count);
            Assert.AreEqual(25, matrixCount);
        }
    
        [Test]
        public void OrderingCachedEnumerableWorksAsExpectedWhileStillKeepingEnumeratedValuesCached()
        {
            _count = 0;
    
            var cachedEnumerable = Enumerable.Range(1, 5).Select(incrementCount).ToCachedEnumerable();
    
            var orderedEnumerated = cachedEnumerable.OrderBy(x => x);
            var orderedEnumeratedArray = orderedEnumerated.ToArray(); // Enumerated first time in ascending order.
            Assert.AreEqual(5, _count);
    
            for (int i = 0; i < orderedEnumeratedArray.Length; i++) {
                Assert.AreEqual(i + 1, orderedEnumeratedArray[i]);
            }
    
            var reorderedEnumeratedArray = orderedEnumerated.OrderByDescending(x => x).ToArray(); // Enumerated second time in descending order.
            Assert.AreEqual(5, _count);
    
            for (int i = 0; i < reorderedEnumeratedArray.Length; i++) {
                Assert.AreEqual(5 - i, reorderedEnumeratedArray[i]);
            }
        }
    
        private int incrementCount(int value)
        {
            _count++;
            return value;
        }
    }
    

    【讨论】:

      【解决方案7】:

      我认为将结果缓存在列表中的想法没有任何严重问题,就像上面的代码一样。可能,使用 ToList() 方法构造列表会更好。

      public IEnumerable<ModuleData> ListModules()
      {
          if (Modules == null)
          {
              Modules = Source.Descendants("Module")
                            .Select(m => new ModuleData(m.Element("ModuleID").Value, 1, 1)))
                            .ToList();
          }
          return Modules;
      }
      

      【讨论】:

      • 这比我的要整洁得多,但是调用 ToList() 无论如何都会迭代整个枚举,所以它不能解决我的问题。
      猜你喜欢
      • 1970-01-01
      • 2018-06-27
      • 1970-01-01
      • 1970-01-01
      • 2011-08-14
      • 1970-01-01
      • 2023-03-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多