構造体で「新規」を使用すると、それをヒープまたはスタックに割り当てますか?


290

new演算子を使用してクラスのインスタンスを作成すると、メモリがヒープに割り当てられます。newオペレーターで構造体のインスタンスを作成すると、ヒープまたはスタックのどこにメモリが割り当てられますか?

回答:


305

わかりました、これをもっと明確にできるか見てみましょう。

まず、Ashは正しいです。問題は、値型変数がどこに割り当てられるかではありません。これは別の質問です。答えは単に「山積み」ではありません。それよりも複雑です(C#2でさらに複雑になっています)。私が持っているトピックに関する記事を、要求された場合は、それを拡張しますが、ただでの契約をしましょう演算子。new

第二に、これらすべては本当にあなたが話しているレベルに依存します。コンパイラーが作成するILの観点から、コンパイラーがソースコードをどのように処理するかを調べています。JITコンパイラーがかなりの「論理」割り当てを最適化するという点で賢明なことを行うことは、可能です。

第三に、私はジェネリックを無視しています。主に私が実際に答えを知らないため、そして部分的には複雑すぎるためです。

最後に、これはすべて、現在の実装についてのものです。C#仕様はこれの多くを指定していません-それは事実上、実装の詳細です。マネージコードの開発者は本当に気にする必要がないと信じている人もいます。どこまで行けるかはわかりませんが、実際にはすべてのローカル変数がヒープ上に存在する世界を想像する価値はあります。


new値型の演算子には2つの異なる状況があります。パラメーターなしのコンストラクター(例new Guid())またはパラメーター付きのコンストラクター(例)を呼び出すことができますnew Guid(someString)。これらは著しく異なるILを生成します。理由を理解するには、C#とCLIの仕様を比較する必要があります。C#によると、すべての値の型にはパラメーターのないコンストラクターがあります。CLI仕様によると、パラメーターのないコンストラクターを持つ値タイプはありません。(しばらくの間リフレクションを使用して値型のコンストラクターをフェッチします。パラメーターのないコンストラクターは見つかりません。)

C#が「値をゼロで初期化する」ことをコンストラクターとして扱うことは理にかなっています。言語を一貫させるためです。つまりnew(...)常にコンストラクターを呼び出すと考えることができます。呼び出す実際のコードはなく、タイプ固有のコードもないので、CLIがそれを別の方法で考えることは理にかなっています。

また、初期化した後の値をどうするかにも違いがあります。使用されるIL

Guid localVariable = new Guid(someString);

使用されるILとは異なります。

myInstanceOrStaticVariable = new Guid(someString);

さらに、値が中間値として使用される場合(メソッド呼び出しへの引数など)、状況は少し異なります。これらの違いをすべて示すために、短いテストプログラムを次に示します。これは、静的変数とインスタンス変数の違いを示していない:ILは、間で異なるだろうstfldstsfld、それがすべてです。

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

これは、無関係なビット(nopsなど)を除いたクラスのILです。

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

ご覧のとおり、コンストラクターの呼び出しにはさまざまな命令が使用されています。

  • newobj:スタックに値を割り当て、パラメーター化されたコンストラクターを呼び出します。中間値に使用されます。たとえば、フィールドへの割り当てやメソッドの引数として使用されます。
  • call instance:既に割り当てられているストレージの場所を使用します(スタック上かどうかに関係なく)。これは、上記のコードでローカル変数に割り当てるために使用されます。同じローカル変数に複数のnew呼び出しを使用して値が複数回割り当てられている場合、古い値の上にデータを初期化するだけで、毎回より多くのスタックスペースを割り当てません
  • initobj:既に割り当てられている保存場所を使用し、データを消去するだけです。これは、ローカル変数に割り当てるものを含め、すべてのパラメーターなしのコンストラクター呼び出しに使用されます。メソッド呼び出しでは、中間ローカル変数が効果的に導入され、その値はによってワイプされinitobjます。

これがトピックがいかに複雑であるかを示していると同時に、少しだけ光を当てているといいのですが。では、いくつかの概念の感覚、すべての呼び出しnewスタック上の割り当てスペース-しかし、我々が見てきたように、実際にも、ILレベルでは何が起こるかではありません。特定のケースを強調したいと思います。この方法を取る:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

その「論理的」には4つのスタック割り当てがあります。1つは変数用で、もう1つは3つのnew呼び出しごとです。しかし、実際には(その特定のコードの場合)スタックは1回だけ割り当てられ、同じストレージの場所が再利用されます。

編集:明確にするために、これは一部の場合にのみ当てはまります...特に、コンストラクターが例外をスローしたguid場合、の値は表示されませんGuid。そのため、C#コンパイラーは同じスタックスロットを再利用できます。詳細と適用されないケースについては、Eric Lippertの値型構築に関するブログ投稿を参照してください。

この回答を書くことで多くのことを学びました-不明な点がある場合は説明を求めてください!


1
ジョン、HowManyStackAllocationsのサンプルコードは優れています。しかし、Guidの代わりにStructを使用するように変更するか、新しいStructの例を追加することができます。そうすれば、@ kedarの元の質問に直接対処できると思います。
Ash

9
Guidはすでに構造体です。msdn.microsoft.com/en-us/library/system.guid.aspx を参照してください。この質問には参照タイプを選択しなかったでしょう:)
Jon Skeet

1
あなたが持っているとき、何が起こるList<Guid>と、それにそれらの3を追加しますか?それは3つの割り当て(同じIL)でしょうか?しかし、それらはどこか魔法のように保たれています
Arec Barrwin '08 / 08/28

1
@アニ:エリックの例にtry / catchブロックがあるという事実を見逃している-したがって、構造体のコンストラクターの間に例外がスローされた場合、コンストラクターの前に値を確認できる必要があります。私の例にそのような状況はありません -コンストラクタが例外で失敗した場合、値guidが半分だけ上書きされたかどうかは関係ありません。
Jon Skeet、2010

2
@アニ:実際、エリックは投稿の下部近くでこれを次のように呼びかけています。コンストラクタの呼び出しと同じレベルの "try"ネストで、新しい一時ファイルを作成し、一時ファイルを初期化して、ローカルにコピーするという厳密な処理は行いません。この特定の(一般的な)ケースでは、 C#プログラムが違いを観察することが不可能であるため、一時ファイルとコピーの作成!」
Jon Skeet、2010

40

構造体のフィールドを含むメモリは、状況に応じてスタックまたはヒープに割り当てることができます。struct-type変数がローカル変数またはパラメーターであり、匿名デリゲートまたはイテレータークラスによってキャプチャされていない場合は、スタックに割り当てられます。変数がクラスの一部である場合、その変数はクラス内のヒープに割り当てられます。

構造体がヒープに割り当てられている場合、メモリを割り当てるためにnew演算子を呼び出す必要は実際にはありません。唯一の目的は、コンストラクターの内容に従ってフィールド値を設定することです。コンストラクターが呼び出されない場合、すべてのフィールドはデフォルト値(0またはnull)を取得します。

スタックに割り当てられた構造体についても同様ですが、C#では、使用する前にすべてのローカル変数を何らかの値に設定する必要があるため、カスタムコンストラクターまたはデフォルトコンストラクター(パラメーターをとらないコンストラクターを常に使用できる)を呼び出す必要があります。構造体)。


13

コンパクトに言えば、newは構造体の誤称であり、newを呼び出すとコンストラクタが呼び出されます。構造体の唯一の保存場所は、定義されている場所です。

メンバー変数の場合は、定義されているものに直接格納されます。ローカル変数またはパラメーターの場合は、スタックに格納されます。

これは、構造体が完全に格納されている場所に参照があるクラスと対照的ですが、参照はヒープ上のどこかを指します。(メンバー内、ローカル/スタック上のパラメーター)

クラスと構造体の間に実際の違いがないC ++を少し調べると役立つ場合があります。(言語には同様の名前がありますが、デフォルトのアクセシビリティを参照するだけです)newを呼び出すと、ヒープの場所へのポインターが取得されますが、ポインター以外の参照がある場合は、スタックに直接格納されます。他のオブジェクト内では、alaはC#の構造体です。


5

すべての値型と同様に、構造体は常に宣言れた場所に移動ます。

構造体をいつ使用するかの詳細については、こちらの質問を参照してください。そしてこの質問はここに、構造体のいくつかの詳細については、で。

編集:私は、彼らがmistankelyと答えていた常にスタックに行きます。これは誤りです。


「構造体は常に宣言された場所に移動します」、これは少し誤解を招く混乱を招きます。クラスの構造体フィールドは、常に「型のインスタンスが構築されるときに動的メモリ」に配置されます-Jeff Richter。これはヒープ上で間接的に発生する可能性がありますが、通常の参照型とはまったく異なります。
Ash

いいえ、それは完全に正しいと思います-それは参照型と同じではありませんが。変数の値は、宣言された場所に存在します。参照型変数の値は、実際のデータではなく参照です。それだけです。
Jon Skeet、

要約すると、メソッド内のどこかに値型を作成(宣言)するときは常に、スタック上に値型が作成されます。
Ash

2
ジョン、あなたは私のポイントを逃しています。この質問が最初に尋ねられた理由は、新しい演算子を使用して作成する場合、構造体が割り当てられる場所が多くの開発者(C#を介してCLRを読むまで含まれていた)には明らかではないためです。「構造体は常に宣言された場所に行く」と言うことは明確な答えではありません。
アッシュ

1
@Ash:時間があれば、仕事に着いたら答えを書こうと思います。しかし、電車の中
Jon Skeet

4

私はおそらくここで何かが足りないのですが、なぜ割り当てを気にするのですか?

値の型は値によって渡される;)なので、定義されている場所とは異なるスコープで変更することはできません。値を変更できるようにするには、[ref]キーワードを追加する必要があります。

参照型は参照によって渡され、変更できます。

もちろん、最も人気のある不変の参照型文字列があります。

配列のレイアウト/初期化:値のタイプ->ゼロメモリ[name、zip] [name、zip]参照タイプ->ゼロメモリ-> null [ref] [ref]


3
参照型は参照によって渡されません-参照は値によって渡されます。それは非常に異なります。
Jon Skeet、2015年

2

A classまたはstruct宣言は、実行時にインスタンスやオブジェクトを作成するために使用される設計図のようなものです。classまたはstruct呼び出されるPerson を定義する場合、Personはタイプの名前です。Person型の変数pを宣言して初期化する場合、pはPersonのオブジェクトまたはインスタンスと呼ばれます。同じPersonタイプの複数のインスタンスを作成することができ、各インスタンスはその中に異なる値を持つことができますpropertiesfields

classは参照型です。のオブジェクトがclassが作成されると、オブジェクトが割り当てられている変数は、そのメモリへの参照のみを保持します。オブジェクト参照が新しい変数に割り当てられると、新しい変数は元のオブジェクトを参照します。1つの変数を介して行われた変更は、両方とも同じデータを参照するため、他の変数に反映されます。

A structは値タイプです。a structが作成されると、structが割り当てられている変数は、構造体の実際のデータを保持します。ときstruct新しい変数に代入され、それがコピーされます。したがって、新しい変数と元の変数には、同じデータの2つの別々のコピーが含まれています。1つのコピーに加えられた変更は、他のコピーには影響しません。

一般に、classesより複雑な動作、またはclassオブジェクトの作成後に変更することを目的としたデータをモデル化するために使用されます。Structsstruct作成後に変更することを意図していない主なデータを含む小さなデータ構造に最適です。

多くのための...


1

値型と見なされる構造体のほとんどはスタックに割り当てられ、オブジェクトはヒープに割り当てられ、オブジェクト参照(ポインタ)はスタックに割り当てられます。


1

構造体はスタックに割り当てられます。ここに役立つ説明があります:

構造

さらに、.NET内でインスタンス化されたクラスは、ヒープまたは.NETの予約済みメモリ空間にメモリを割り当てます。一方、構造体は、スタックへの割り当てが原因で、インスタンス化時に効率が向上します。さらに、構造体内のパラメーターの受け渡しは値によって行われることに注意してください。


5
これは、構造体がクラスの一部である場合はカバーしません。その時点で、構造体はヒープ上に存在し、オブジェクトの残りのデータが存在します。
Jon Skeet、

1
はい、しかしそれは実際に焦点を当てられ、尋ねられている質問に答えます。投票した。
アッシュ

...まだ不正確で誤解を招きやすい。申し訳ありませんが、この質問に対する短い答えはありません-ジェフリーが唯一の完全な答えです。
Marc Gravell
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.