一个有趣的挑战,所以我必须提供自己的解决方案。事实上非常有趣,我的解决方案现在是版本 3。版本 2 是我根据 Servy 的反馈进行的简化。然后我意识到我的解决方案有很大的缺点。如果缓存的可枚举的第一个枚举没有完成,则不会进行缓存。许多 LINQ 扩展都喜欢First
并且Take
只会枚举足够的可枚举来完成工作,我必须更新到版本 3 才能使这项工作与缓存一起使用。
问题是关于不涉及并发访问的可枚举的后续枚举。尽管如此,我还是决定让我的解决方案线程安全。它增加了一些复杂性和一些开销,但应该允许在所有场景中使用该解决方案。
public static class EnumerableExtensions {
public static IEnumerable<T> Cached<T>(this IEnumerable<T> source) {
if (source == null)
throw new ArgumentNullException("source");
return new CachedEnumerable<T>(source);
}
}
class CachedEnumerable<T> : IEnumerable<T> {
readonly Object gate = new Object();
readonly IEnumerable<T> source;
readonly List<T> cache = new List<T>();
IEnumerator<T> enumerator;
bool isCacheComplete;
public CachedEnumerable(IEnumerable<T> source) {
this.source = source;
}
public IEnumerator<T> GetEnumerator() {
lock (this.gate) {
if (this.isCacheComplete)
return this.cache.GetEnumerator();
if (this.enumerator == null)
this.enumerator = source.GetEnumerator();
}
return GetCacheBuildingEnumerator();
}
public IEnumerator<T> GetCacheBuildingEnumerator() {
var index = 0;
T item;
while (TryGetItem(index, out item)) {
yield return item;
index += 1;
}
}
bool TryGetItem(Int32 index, out T item) {
lock (this.gate) {
if (!IsItemInCache(index)) {
// The iteration may have completed while waiting for the lock.
if (this.isCacheComplete) {
item = default(T);
return false;
}
if (!this.enumerator.MoveNext()) {
item = default(T);
this.isCacheComplete = true;
this.enumerator.Dispose();
return false;
}
this.cache.Add(this.enumerator.Current);
}
item = this.cache[index];
return true;
}
}
bool IsItemInCache(Int32 index) {
return index < this.cache.Count;
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
扩展名是这样使用的(sequence
是一个IEnumerable<T>
):
var cachedSequence = sequence.Cached();
// Pulling 2 items from the sequence.
foreach (var item in cachedSequence.Take(2))
// ...
// Pulling 2 items from the cache and the rest from the source.
foreach (var item in cachedSequence)
// ...
// Pulling all items from the cache.
foreach (var item in cachedSequence)
// ...
如果仅枚举部分可枚举对象(例如cachedSequence.Take(2).ToList()
. 使用的枚举器ToList
将被释放,但底层源枚举器未释放。这是因为前 2 项被缓存并且源枚举器保持活动状态应该对后续项目的请求。在这种情况下,源枚举器仅在符合垃圾收集条件时才被清理(这将与可能的大缓存相同)。