public IEnumerable<ModuleData> ListModules()
{
foreach (XElement m in Source.Descendants("Module"))
{
yield return new ModuleData(m.Element("ModuleID").Value);
}
}
Initially the above code is great since there is no need to evaluate the entire collection if it is not needed.
However, once all the Modules have been enumerated once, it becomes more expensive to repeatedly query the XDocument when there is no change.
So, as a performance improvement:
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;
}
Which is great if I am repeatedly using the entire list but not so great otherwise.
Is there a middle ground where I can yield return until the entire list has been iterated, then cache it and serve the cache to subsequent requests?
You can look at Saving the State of Enumerators which describes how to create lazy list (which caches once iterated items).
Check out MemoizeAll()
in the Reactive Extensions for .NET library (Rx). As it is evaluated lazily you can safely set it up during construction and just return Modules
from ListModules()
:
Modules = Source.
Descendants("Module").
Select(m => new ModuleData(m.Element("ModuleID").Value, 1, 1)).
MemoizeAll();
There's a good explanation of MemoizeAll()
(and some of the other less obvious Rx extensions) here.
I like @tsemer's answer. But I would like to propose my solutions, which has nothing to do with FP. It's naive approach, but it generates a lot less of allocations. And it is not thread safe.
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();
}
}
This is how the matrix test from @tsemer's answer will work:
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
}
}
x
) skips first for
, because _cache
is empty;x
fetches one item from the _enumerator
to the _cache
;x
pauses before second for
loop;y
) enumerates one element from the _cache
;y
fetches all elements from the _enumerator
to the _cache
;y
skips the third for
loop, because its index
variable equals 5
;x
resumes, its index
equals 1
. It skips the second for
loop because _enumerator
is finished;x
enumerates one element from the _cache
using the third for
loop; x
pauses before the third for
;y
enumerates 5 elements from the _cache
using first for
loop;y
skips the second for
loop, because _enumerator
is finished;y
skips the third for
loop, because index
of y
equals 5
;x
resumes, increments index
. It fetches one element from the _cache
using the third for
loop.x
pauses.index
variable of x
is less than 5
then go to 10;If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With