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

如果我重复使用整个列表,这很好,但否则就不是很好。

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

4

7 回答 7

8

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

于 2009-10-08T11:09:52.790 回答
7

查看Reactive Extensions for .NETMemoizeAll()库(Rx)。由于它是懒惰地评估的,您可以在施工期间安全地设置它,然后从以下位置返回:ModulesListModules()

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

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

于 2010-11-03T09:03:44.347 回答
7

我喜欢@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. xfor在第二个循环之前暂停;
  4. 内部循环 ( ) 从;y中枚举一个元素。_cache
  5. y获取从_enumerator到 的所有元素_cache
  6. y跳过第三个for循环,因为它的index变量等于5;
  7. x简历,其index等于1。它跳过第二个for循环,因为_enumerator已完成;
  8. x_cache使用第三个for循环 枚举一个元素;
  9. x在第三个之前停顿for
  10. y_cacheusing firstfor循环中枚举 5 个元素;
  11. y跳过第二个for循环,因为_enumerator已完成;
  12. y跳过第三个for循环,index因为yequals 5;
  13. x恢复,增量index。它从_cache使用第三个for循环中获取一个元素。
  14. x停顿。
  15. 如果index变量 ofx小于5则转到 10;
  16. 结尾。
于 2016-01-06T12:42:37.013 回答
5

我已经看到了一些实现,一些较旧并且没有利用最新的 .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;
  }
}
于 2016-01-06T11:36:29.330 回答
4

我非常喜欢 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;
                }
            }
        }
    }

是的,您可以通过产生电流使其效率更高...但是我会受到微秒的打击...每个元素只会发生一次。

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

于 2016-12-19T12:48:45.450 回答
1

简单总结一下:

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

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

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;
    }
}
于 2020-06-25T15:09:54.437 回答
-1

我认为将结果缓存在列表中的想法没有任何严重问题,就像上面的代码一样。可能,使用 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;
}
于 2009-10-08T11:12:39.513 回答