2009-10-08 16 views
17
public IEnumerable<ModuleData> ListModules() 
{ 
    foreach (XElement m in Source.Descendants("Module")) 
    { 
     yield return new ModuleData(m.Element("ModuleID").Value); 
    } 
} 

Początkowo powyższy kod jest świetny, ponieważ nie ma potrzeby oceny całej kolekcji, jeśli nie jest potrzebna.Buforowanie IEnumerable

Jednak, gdy wszystkie moduły zostaną wyliczone jeden raz, droższe staje się wielokrotne wysyłanie zapytań do XDocument, gdy nie ma żadnych zmian.

Tak, jak poprawa wydajności:

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

co jest dobre, jeśli ja wielokrotnie stosując całą listę, ale nie tak wielki inaczej.

Czy istnieje pośrednie pole, na którym można uzyskać zwrot do momentu, aż cała lista zostanie powtórzona, a następnie zapisana w pamięci podręcznej i udostępniona w pamięci podręcznej kolejnym żądaniom?

+1

Czy mam coś takiego. źle? Twój kod wydaje się robić dokładnie to, o co prosisz ... –

+1

Drugi blok kodu będzie zawsze iterować cały wyliczalny, nawet jeśli nie jest to wymagane. – djskinner

Odpowiedz

8

Można spojrzeć na Saving the State of Enumerators, który opisuje, jak utworzyć leniwą listę (która buforuje po elementach iterowanych).

+0

bardzo fajne! dzięki za link to całkowicie rozwiązało podobny problem, jaki miałem z zapytaniem odczytanym z dysku. – luke

+0

Dla potomności, czy mógłbyś dołączyć odpowiednie części linku, które okazały się przydatne w twojej odpowiedzi? W ten sposób, jeśli link się zmniejszy, zmiany itp., Twoja odpowiedź nie będzie bezużyteczna. Wielkie dzięki. –

-1

Nie widzę poważnego problemu z pomysłem buforowania wyników na liście, tak jak w powyższym kodzie. Prawdopodobnie lepiej byłoby skonstruować listę za pomocą metody 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; 
} 
+0

To jest o wiele porządniejsze niż moje, ale wywołanie ToList() iteruje cały wyliczalny, więc nie rozwiązuje mojego problemu. – djskinner

4

Wyjazd MemoizeAll() w Reactive Extensions for .NET biblioteki (Rx). Jak jest oceniana leniwie można bezpiecznie ustawić go w trakcie budowy i po prostu wrócić Modules z ListModules():

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

Jest to dobre wyjaśnienie MemoizeAll() (i kilka innych mniej oczywistych rozszerzeń Rx) here.

+0

To jest bardzo ładne, lubię korzystać z Rx. Nadal staram się znaleźć czas i wymówkę, żeby lepiej się z nim bawić. – djskinner

3

Widziałem kilka implementacji tam, niektóre starsze i nie korzystające z najnowszych klas .Net, niektóre zbyt wyszukane dla moich potrzeb. Skończyłem na najbardziej zwięzłym i deklaratywnym kodzie, jaki udało mi się zebrać, co dodało do klasy około 15 linii kodu (rzeczywistego). Wydaje się również dostosowanie do potrzeb OP:

EDIT: Druga rewizja, lepsze wsparcie dla pustych enumerables

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

Przydatną metodę rozszerzenia mogą być:

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

A dla jednostki testerzy między wami: (jeśli nie korzystasz z programu resharper, po prostu usuń atrybuty [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; 
    } 
} 
2

Lubię odpowiedź @ tsemer. Ale chciałbym zaproponować moje rozwiązania, które nie mają nic wspólnego z PR. To naiwne podejście, ale generuje dużo mniej przydziałów. I to nie jest bezpieczne dla wątków.

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

ten sposób testy matrycy z użytkownika @ tsemer odpowiedź będzie działać:

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. Zewnętrzna pętla (x) pomija pierwszą for, ponieważ _cache jest pusty;
  2. x pobiera jedną pozycję z _enumerator do _cache;
  3. x zatrzymuje się przed drugą pętlą for;
  4. Wewnętrzna pętla (y) wylicza jeden element z _cache;
  5. y pobiera wszystkie elementy z _enumerator do _cache;
  6. y pomija trzecią pętlę for, ponieważ jej zmienna index jest równa 5;
  7. x CV, jego index jest równy 1. Pomija drugą pętlę for, ponieważ zakończyła się _enumerator;
  8. x wylicza jeden element z _cache przy użyciu trzeciej pętli for;
  9. x zatrzymuje się przed trzecim for;
  10. y wylicza 5 elementów z _cache przy użyciu pierwszej pętli for;
  11. y pomija drugą pętlę for, ponieważ zakończyła się _enumerator;
  12. y pomija trzecią pętlę for, ponieważ index z y jest równa 5;
  13. x życiorysy, przyrosty index. To pobiera jeden element z _cache przy użyciu trzeciej pętli for.
  14. x zatrzymuje się.
  15. jeśli zmienna index zmienna z x jest mniejsza niż 5, a następnie przejdź do 10;
  16. koniec.
+0

Ładne i czyste, a także podoba mi się to, że to rozwiązanie nie wylicza pierwszego elementu podczas tworzenia instancji. – tsemer

+0

Wygląda czysto i prosto. Czy mógłbyś dodać wyjaśnienie, dlaczego trzeci blok 'for' jest potrzebny? – djskinner

+1

@djskinner Dodałem trochę informacji – hazzik

1

Ja całkiem lubię odpowiedź hazzika ... miła i prosta zawsze jest drogą. ALE jest błąd w GetEnumeratorze

to rodzaj realizuje jest problem, i dlatego istnieje dziwne 3rd pętli po drugiej pętli egzumeratora .... ale to nie jest tak proste, jak to. Problem, który wyzwala trzecią pętlę, jest ogólny ... więc musi być rekurencyjny.

Odpowiedź jest jeszcze prostsza.

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

tak można zrobić to odrobinę bardziej wydajne ulegając prąd ... ale wezmę trafienie mikrosekund ... to tylko zdarzy raz na elemencie.

i nie jest to wątek bezpieczny ... ale kogo to obchodzi.