4

LINQ クエリで ToList() または ToArray() を呼び出す方が良いですか?という質問を読んでいました。そして、動的にサイズを変更する内部クラスを使用する代わりに、Enumerable.ToArray()最初にメソッドを呼び出してコレクションのサイズを見つけないのはなぜだろうと思っていました。次のようなもの:Count()Buffer{T}

T[] ToArray<T>(IEnumerable<T> source)
{
    var count = source.Count();
    var array = new T[count];

    int index = 0;
    foreach (var item in source) array[index++] = item;
    return array;
}

設計者や実装者の頭の中がどうなっているのかわからないことはわかっていますが、彼らは私よりずっと賢いと確信しています。この質問をする最善の方法は、上記のアプローチの何が問題なのかということです。メモリ割り当てが少ないようで、それでも O(n) 時間で動作します。

4

4 に答える 4

4

Buffer<T>クラスには、ソース シーケンスが実装する場合の最適化がありますICollection<T>

internal Buffer(IEnumerable<TElement> source)
{
   int length = 0;
   TElement[] array = null;
   ICollection<TElement> collection = source as ICollection<TElement>;
   if (collection != null)
   {
      length = collection.Count;
      if (length > 0)
      {
         array = new TElement[length];
         collection.CopyTo(array, 0);
      }
   }
   else
   {
      ...

シーケンスが を実装していない場合ICollection<T>、コードはシーケンスを 2 回列挙しても安全であると想定できないため、必要に応じて配列のサイズ変更に戻ります。

于 2013-06-18T15:53:35.170 に答える
4

まず、クラス コンストラクターは、指定されたシーケンスを(配列やリストのように)プロパティを持つBuffer<T>キャストできるかどうかも最適化します。ICollectionCount

TElement[] array = null;
int num = 0;
ICollection<TElement> collection = source as ICollection<TElement>;
if (collection != null)
{
    num = collection.Count;
    if (num > 0)
    {
        array = new TElement[num];
        collection.CopyTo(array, 0);
    }
}
else
    // now we are going the long way ...

したがって、コレクションでない場合は、クエリを実行して合計数を取得する必要があります。しかしEnumerable.Count、正しいサイズの配列を初期化するためだけに使用すると、非常にコストがかかり、さらに重要なことに、危険な副作用が生じる可能性があります。したがって、安全ではありません。

次の簡単なFile.ReadLines例を考えてみましょう。

var lines = File.ReadLines(path);
int count = lines.Count(); // executes the query which also disposes the underlying IO.TextReader 
var array = new string[count];
int index = 0;
foreach (string line in lines) array[index++] = line;

これは、クエリが既に実行されているObjectDisposedExceptionため、「閉じられた TextReader から読み取ることができません」をスローします。その間、リーダーは で破棄されます。lines.Count()foreach

于 2013-06-18T15:53:55.980 に答える
1

Count() はソースを最後まで列挙するためです。したがって、少なくとも 2 回の繰り返しが行われます。1 回はカウント用で、もう 1 回はアイテムのコピー用です。

ここで、問題の列挙型がデータベース カーソル、または反復時に重要な操作を含む類似のものであると考えてください。それはパフォーマンスキラーです。

バッファを memcopy して拡張する方が良い方法です。パフォーマンスにわずかな影響を与える可能性がありますが、非常に小さく、より重要なのは既知の量です。

于 2013-06-18T15:46:56.943 に答える
0

IEnumerable<T>および/またはIEnumerator<T>プロパティとともに、そのカウントを「知っている」かどうかを尋ねるプロパティを含めていた場合、そのようなことを利用するCount価値があったかもしれません[ beの一部を持つことは、たとえば呼び出しスレッドセーフな可変型では、スナップショットが列挙されます]。そのような機能がなければ、コードにorがあっても、追加の一時配列を作成するよりも呼び出しにかかる時間が長いか短いかを知る方法はありません。ToArray()CountIEnumerator<T>GetEnumeratorICollectionICollection<T>Count

そうは言っても、次のようなものの最適な実装は、ToArrayおそらく、それぞれがいくつかのアイテムを保持するリンクされたリストを使用することであり、各アイテムがそれが占有するスペースに読み込まれるようになると予想します。最終配列にコピーできる時間。85,000 バイトを超える配列を作成するよりも、情報を複数の小さな配列に分割する方がよいため、の 2 倍の戦略はList<T>特に適切ではないようです (一時的な配列は、終了すると役に立たなくなるため)。ラージ オブジェクト ヒープでは特に問題になります)。

于 2013-06-18T16:25:36.613 に答える