Enumerable.ToList(source)
は、本質的に単なる呼び出しnew List(source)
です。
このコンストラクターは、 source が であるかどうかをテストしICollection<T>
、そうである場合は適切なサイズの配列を割り当てます。それ以外の場合、つまりソースが LINQ クエリであるほとんどの場合、デフォルトの初期容量 (4 項目) で配列を割り当て、必要に応じて容量を 2 倍にして拡張します。容量が 2 倍になるたびに、新しいアレイが割り当てられ、古いアレイが新しいアレイにコピーされます。
これにより、リストに多数のアイテムが含まれる場合 (おそらく少なくとも数千個) にオーバーヘッドが発生する可能性があります。リストが 85 KB を超えるとすぐにオーバーヘッドが大きくなる可能性があります。これは、圧縮されず、メモリの断片化が発生する可能性があるラージ オブジェクト ヒープにリストが割り当てられるためです。リスト内の配列を参照していることに注意してください。が参照型の場合T
、その配列には参照のみが含まれ、実際のオブジェクトは含まれません。これらのオブジェクトは、85 KB の制限にはカウントされません。
シーケンスのサイズを正確に見積もることができれば、このオーバーヘッドの一部を取り除くことができます (少し過小評価するよりも、少し過大評価する方が適切です)。たとえば、.Select()
を実装するものに対してのみ演算子を実行してICollection<T>
いる場合、出力リストのサイズがわかります。
そのような場合、この拡張メソッドはこのオーバーヘッドを削減します:
public static List<T> ToList<T>(this IEnumerable<T> source, int initialCapacity)
{
// parameter validation ommited for brevity
var result = new List<T>(initialCapacity);
foreach (T item in source)
{
result.Add(item);
}
return result;
}
場合によっては、作成したリストが、以前の実行などですでに存在していたリストを置き換えるだけになることがあります。そのような場合、古いリストを再利用すると、かなりの数のメモリ割り当てを回避できます。ただし、その古いリストに同時にアクセスできない場合にのみ機能します。新しいリストが通常古いリストよりも大幅に小さい場合は、そうしません。その場合は、次の拡張メソッドを使用できます。
public static void CopyToList<T>(this IEnumerable<T> source, List<T> destination)
{
// parameter validation ommited for brevity
destination.Clear();
foreach (T item in source)
{
destination.Add(item);
}
}
そうは言っても、私.ToList()
は非効率的だと思いますか?いいえ、メモリがあり、リストを繰り返し使用する場合は、リストにランダムにインデックスを付けたり、複数回繰り返したりします。
特定の例に戻ります。
var matches = (from x in list1 join y in list2 on x equals y select x).ToList();
他の方法でこれを行う方が効率的な場合があります。たとえば、次のようになります。
var matches = list1.Intersect(list2).ToList();
list1 と list2 に重複が含まれていない場合は同じ結果が得られ、list2 が小さい場合は非常に効率的です。
ただし、いつものように、実際に知る唯一の方法は、典型的なワークロードを使用して測定することです。