楽しい挑戦なので、私自身の解決策を提供する必要があります。実際、私のソリューションは現在バージョン 3 になっています。バージョン 2 は、Servy からのフィードバックに基づいて単純化したものです。その後、私のソリューションには大きな欠点があることに気付きました。キャッシュされた列挙型の最初の列挙が完了しなかった場合、キャッシュは行われません。多くの LINQ 拡張機能は、ジョブを完了するのに十分な数の列挙可能なものだけを列挙します。これをキャッシュで機能させるには、バージョン 3 に更新する必要がありましたFirst
。Take
問題は、同時アクセスを伴わない列挙可能な列挙の後続の列挙に関するものです。それにもかかわらず、ソリューションをスレッドセーフにすることにしました。多少の複雑さと多少のオーバーヘッドが追加されますが、ソリューションをすべてのシナリオで使用できるようにする必要があります。
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 つの項目がキャッシュされ、ソース列挙子が有効に保たれるためです。その場合、ソース列挙子は、ガベージ コレクションの対象となる場合にのみクリーンアップされます (これは、おそらく大きなキャッシュと同じ時間になります)。