7

次のコードがあるとします。

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
");
Enumerable.Range (1, 100)
    .Select (s => X.Elements ()
        .Select (t => Int32.Parse (t.Attribute ("v").Value))
        .Aggregate (s, (t, u) => t * u)
    )
    .ToList ()
    .ForEach (s => Console.WriteLine (s));

.NETランタイムは実際にここで何をしていますか?属性を解析して100回ごとに整数に変換しますか、それとも、解析された値をキャッシュし、範囲内の各要素に対して計算を繰り返さないようにする必要があることを理解するのに十分賢いですか?

さらに、どうすればこのようなことを自分で考え出すことができますか?

よろしくお願いします。

4

2 に答える 2

4

LINQ であり、IEnumerable<T>プルベースです。これは、一般的に LINQ ステートメントの一部である述語とアクションは、値が取得されるまで実行されないことを意味します。さらに、述語とアクションは、値が取得されるたびに実行されます (たとえば、秘密のキャッシュは行われません)。

からのプルは、値をプルするために呼び出しと繰り返し呼び出しによって列挙子を取得するための実際の構文糖衣でIEnumerable<T>あるステートメントによって行われます。foreachIEnumerable<T>.GetEnumerator()IEnumerator<T>.MoveNext()

ToList()ToArray()、などのLINQ 演算子はステートメントToDictionary()ToLookup()ラップするため、これらのメソッドはプルを実行します。、 、などforeachの演算子についても同じことが言えます。これらのメソッドには、ステートメントを実行して作成する必要がある単一の結果を生成するという共通点があります。Aggregate()Count()First()foreach

多くの LINQ 演算子は、新しいIEnumerable<T>シーケンスを生成します。結果のシーケンスから要素がプルされる場合、オペレーターはソース シーケンスから 1 つ以上の要素をプルします。演算子は最もSelect()明白な例ですが、他の例としてはSelectMany()、、、、、Where()およびがあります。これらのオペレーターはキャッシュを行いません。N 番目の要素が aから取得されると、ソース シーケンスから N 番目の要素が取得され、指定されたアクションを使用して射影が適用され、それが返されます。ここでは何も秘密はありません。Concat()Union()Distinct()Skip()Take()Select()

他の LINQ 演算子も新しいIEnumerable<T>シーケンスを生成しますが、それらは実際にソース シーケンス全体をプルし、ジョブを実行してから新しいシーケンスを生成することによって実装されます。これらの方法にはReverse()、 、OrderBy()およびが含まれGroupBy()ます。ただし、オペレーターによって行われるプルは、オペレーター自体がプルされたときにのみ実行されます。つまり、foreach何かを実行する前に、LINQ ステートメントの「最後」にループが必要です。これらのオペレーターは、ソース シーケンス全体をすぐにプルするため、キャッシュを使用していると主張できます。ただし、このキャッシュはオペレーターが繰り返されるたびに構築されるため、実際には実装の詳細であり、同じOrderBy()操作を同じシーケンスに複数回適用していることを魔法のように検出するものではありません。


あなたの例でToList()は、プルを行います。外側のアクションはSelect100 回実行されます。このアクションが実行されるたびAggregate()に、XML 属性を解析する別のプルが実行されます。合計で、コードはInt32.Parse()200 回呼び出されます。

これを改善するには、反復ごとに属性を取得するのではなく、一度属性を取得します。

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
")
.Elements ()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList ();
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList () 
    .ForEach (s => Console.WriteLine (s)); 

Int32.Parse()は2回しか呼び出されません。ただし、コストは、属性値のリストを割り当てて保存し、最終的にガベージ コレクションを行わなければならないことです。(リストに 2 つの要素が含まれている場合は、大きな問題ではありません。)

属性をプルする最初のものを忘れた場合ToList()でも、コードは実行されますが、元のコードとまったく同じパフォーマンス特性であることに注意してください。属性を格納するためにスペースは使用されませんが、反復ごとに解析されます。

于 2012-04-25T09:23:22.920 に答える
2

このコードを掘り下げてからしばらく経ちましたが、IIRC では、提供されたコードSelectを単純にキャッシュFuncし、一度に 1 つずつソース コレクションで実行する方法が機能します。そのため、外側の範囲の各要素に対してSelect/Aggregate、最初の場合と同様に内側のシーケンスが実行されます。組み込みのキャッシュは行われていません。式で自分で実装する必要があります。

これを自分で理解したい場合は、次の 3 つの基本的なオプションがあります。

  1. コードをコンパイルしildasm、IL を表示するために使用します。これは最も正確ですが、特にラムダとクロージャの場合、IL から得られるものは、C# コンパイラに入力したものとはまったく異なる場合があります。
  2. dotPeek などを使用して System.Linq.dll を C# に逆コンパイルします。繰り返しますが、これらの種類のツールから得られるものは、元のソース コードにほぼ似ているだけかもしれませんが、少なくとも C# になります (特に、dotPeek はかなりうまく機能し、無料です)。
  3. 私の個人的な好み - .NET 4.0 Reference Sourceをダウンロードして、自分で探してください。これが目的です:)参照ソースがバイナリの生成に使用された実際のソースと一致することをMSを信頼する必要がありますが、それらを疑う正当な理由はありません。
  4. @AllonGuralnek で指摘されているように、1 行内の特定のラムダ式にブレークポイントを設定できます。カーソルをラムダ本体のどこかに置いて F9 を押すと、ラムダだけにブレークポイントが設定されます。(間違っていると、行全体がブレークポイントの色で強調表示されます。正しく実行すると、ラムダだけが強調表示されます。)
于 2012-04-25T02:43:48.290 に答える