缓存IEnumerable

public IEnumerable ListModules() { foreach (XElement m in Source.Descendants("Module")) { yield return new ModuleData(m.Element("ModuleID").Value); } } 

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

但是,一旦枚举了所有模块,在没有更改时重复查询XDocument会变得更加昂贵。

所以,作为绩效改进:

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

如果我反复使用整个列表,那就太好了,但不是那么好。

是否存在中间点,我可以在整个列表被迭代之前返回,然后缓存它并将缓存提供给后续请求?

您可以查看保存枚举器状态,其中描述了如何创建惰性列表(缓存项目时缓存一次)。

查看.NET库(Rx)的Reactive Extensions中的MemoizeAll() )。 由于它被懒惰地评估,你可以在构造期间安全地设置它,并从ListModules()返回Modules

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

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

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

 public class CachedEnumerable : IEnumerable, IDisposable { IEnumerator _enumerator; readonly List _cache = new List(); public CachedEnumerable(IEnumerable enumerable) : this(enumerable.GetEnumerator()) { } public CachedEnumerable(IEnumerator enumerator) { _enumerator = enumerator; } public IEnumerator 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(ints); foreach (var x in cachedEnumerable) { foreach (var y in cachedEnumerable) { //Do something } } 
  1. 外部循环( x )首先跳过,因为_cache为空;
  2. x_enumerator获取一个项目到_cache ;
  3. x在第二次循环之前暂停;
  4. 内循环( y )枚举_cache一个元素;
  5. y_enumerator获取所有元素到_cache ;
  6. y跳过第三个for循环,因为它的index变量等于5 ;
  7. x恢复,其index等于1 。 它跳过第二个for循环,因为_enumerator已完成;
  8. x使用第三个for循环枚举_cache中的一个元素;
  9. x在第三次之前暂停;
  10. y使用first for循环枚举_cache 5个元素;
  11. y跳过第二个for循环,因为_enumerator已完成;
  12. y跳过第三个for循环,因为y index等于5 ;
  13. x恢复,递增index 。 它使用第三个for循环从_cache获取一个元素。
  14. x暂停。
  15. 如果x index变量小于5则转到10;
  16. 结束。

我已经看到了一些实现,一些更老,没有利用最新的.Net类,有些太精心设计了我的需求。 我最终得到了我能够集中的最简洁和声明性的代码,它增加了一个包含大约15行(实际)代码的类。 它似乎与OP的需求很好地吻合:

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

 ///  /// A  that caches every item upon first enumeration. ///  ///  ///  public class CachedEnumerable : IEnumerable { 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> _nextItems; ///  /// Initialises a new instance of  using  as the current item /// and  as a value factory for the  containing the next items. ///  protected internal CachedEnumerable(T item, Func> nextItems) { _hasItem = true; _item = item; _nextItems = new Lazy>(nextItems); } ///  /// Initialises a new instance of  with no current item and no next items. ///  protected internal CachedEnumerable() { _hasItem = false; } ///  /// Instantiates and returns a  for a given . /// Notice: The first item is always iterated through. ///  public static CachedEnumerable Create(IEnumerable enumerable) { return Create(enumerable.GetEnumerator()); } ///  /// Instantiates and returns a  for a given . /// Notice: The first item is always iterated through. ///  private static CachedEnumerable Create(IEnumerator enumerator) { return enumerator.MoveNext() ? new CachedEnumerable(enumerator.Current, () => Create(enumerator)) : new CachedEnumerable(); } ///  /// Returns an enumerator that iterates through the collection. ///  public IEnumerator GetEnumerator() { if (_hasItem) { yield return _item; var nextItems = _nextItems.Value; if (nextItems != null) { foreach (var nextItem in nextItems) { yield return nextItem; } } } } ///  /// Returns an enumerator that iterates through a collection. ///  IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } 

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

 public static class IEnumerableExtensions { ///  /// Instantiates and returns a  for a given . /// Notice: The first item is always iterated through. ///  public static CachedEnumerable ToCachedEnumerable(this IEnumerable enumerable) { return CachedEnumerable.Create(enumerable); } } 

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

 ///  /// Tests the  class. ///  [TestFixture] public class CachedEnumerableTest { private int _count; ///  /// This test case is only here to emphasise the problem with  which  attempts to solve. ///  [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); } ///  /// This test case is only here to emphasise the problem with  which  attempts to solve. ///  [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); } ///  /// 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 ///  [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; } } 

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

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

答案虽然看起来更简单。

  public IEnumerator 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; } } } } 

是的,你可以通过产生电流使它变得更有效率......但是我将采取微秒级命中......每个元素只发生一次。

而且它不是线程安全的...但是谁在乎这个问题。

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

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