web-dev-qa-db-ja.com

C#でforeachループを使用する場合のメモリ割り当て

ForeachループがC#でどのように機能するかについての基本を知っています( foreachループがC#でどのように機能するか

Foreachを使用すると、ガベージコレクションが発生する可能性のあるメモリが割り当てられるかどうか疑問に思っていますか? (すべての組み込みシステムタイプ用)。

たとえば、System.Collections.Generic.List<T>クラスでReflectorを使用すると、GetEnumeratorの実装は次のようになります。

public Enumerator<T> GetEnumerator()
{
    return new Enumerator<T>((List<T>) this);
}

使用するたびに、これは新しい列挙子(およびより多くのガベージ)を割り当てます。

すべてのタイプがこれを行いますか?もしそうなら、なぜですか? (単一の列挙子を再利用することはできませんか?)

21
lysergic-acid

Foreachは割り当てを引き起こす可能性がありますが、少なくとも新しいバージョンの.NETおよびMonoでは、具体的な_System.Collections.Generic_型または配列を扱っている場合はそうではありません。これらのコンパイラの古いバージョン(5.5までUnity3Dで使用されていたMonoのバージョンなど)は常に割り当てを生成します。

C#コンパイラは ダックタイピング を使用してGetEnumerator()メソッドを検索し、可能であればそれを使用します。 _System.Collection.Generic_型のほとんどのGetEnumerator()メソッドには、構造体を返すGetEnumerator()メソッドがあり、配列は特別に処理されます。 GetEnumerator()メソッドが割り当てられない場合は、通常、割り当てを回避できます。

ただし、インターフェースIEnumerable、_IEnumerable<T>_、IList、または_IList<T>_のいずれかを処理している場合は、常に割り当てが取得されます。実装クラスが構造体を返す場合でも、構造体はボックス化され、IEnumeratorまたは_IEnumerator<T>_にキャストされます。これには割り当てが必要です。


注:Unity 5.5がC#6に更新されて以来、この2番目の割り当てがまだ残っている現在のコンパイラリリースはありません。

理解するのが少し複雑な2番目の割り当てがあります。このforeachループを取ります:

_List<int> valueList = new List<int>() { 1, 2 };
foreach (int value in valueList) {
    // do something with value
}
_

C#5.0までは、次のように拡張されます(わずかな違いはあります)。

_List<int>.Enumerator enumerator = valueList.GetEnumerator();
try {
    while (enumerator.MoveNext()) {
        int value = enumerator.Current;
        // do something with value
    }
}
finally {
    IDisposable disposable = enumerator as System.IDisposable;
    if (disposable != null) disposable.Dispose();
}
_

_List<int>.Enumerator_は構造体であり、ヒープに割り当てる必要はありませんが、キャスト_enumerator as System.IDisposable_は構造体をボックス化します。これは割り当てです。仕様はC#5.0で変更され、割り当てが禁止されましたが、.NET 仕様を破った そして以前に割り当てを最適化しました。

これらの割り当ては非常にわずかです。割り当てはメモリリークとは大きく異なり、ガベージコレクションを使用すると、通常は心配する必要がないことに注意してください。ただし、これらの割り当てでさえ気にするシナリオがいくつかあります。私はUnity3Dの作業を行っていますが、5.5までは、ガベージコレクターを実行すると目立った問題が発生するため、ゲームフレームごとに発生する操作を割り当てることができませんでした。

配列のforeachループは特別に処理され、Disposeを呼び出す必要がないことに注意してください。私の知る限り、配列をループするときにforeachが割り当てられたことはありません。

53
hangar

いいえ、リストを列挙してもガベージコレクションは発生しません。

List<T>クラスの列挙子は、ヒープからメモリを割り当てません。これはクラスではなく構造体であるため、コンストラクターはオブジェクトを割り当てず、値を返すだけです。 foreachコードは、その値をヒープではなくスタックに保持します。

ただし、他のコレクションの列挙子はクラスである可能性があり、ヒープにオブジェクトを割り当てます。確実にするには、各ケースの列挙子のタイプを確認する必要があります。

5
Guffa

列挙子が現在のアイテムを保持しているためです。データベースと比較すると、カーソルのようなものです。複数のスレッドが同じ列挙子にアクセスすると、シーケンスを制御できなくなります。また、foreachが参照するたびに、最初の項目にリセットする必要があります。

1

コメントで述べたように、これはガベージコレクションのポイントであるため、通常、心配する必要のある問題ではありません。そうは言っても、私の理解では、各foreachループは新しい列挙子オブジェクトを生成し、最終的にはガベージコレクションされます。理由を確認するには、インターフェースのドキュメントを参照してください ここ 。ご覧のとおり、次のオブジェクトを要求する関数呼び出しがあります。これを実行できるということは、列挙子に状態があることを意味し、次の状態を認識している必要があります。これが必要な理由については、コレクションアイテムのすべての順列間で相互作用を引き起こしている画像:

foreach(var outerItem in Items)
{
   foreach(var innterItem in Items)
   {
      // do something
   }
}

ここでは、同じコレクションに同時に2つの列挙子があります。明らかに、共有された場所はあなたの目標を達成しません。

1
Andrew Ring