web-dev-qa-db-ja.com

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

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

277
kedar kamthe

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

まず、Ashが正しい:問題はnot値型variablesが割り当てられます。それは別の質問です-そして答えは「スタック上」だけではありません。それよりも複雑です(C#2でさらに複雑になりました)。 トピックに関する記事 があり、要求があればそれを展開しますが、new演算子だけを扱いましょう。

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

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

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


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

C#が「値をゼロで初期化する」をコンストラクターとして扱うのは理にかなっています。言語の一貫性を保つためです-new(...)alwaysコンストラクターの呼び出し。呼び出す実際のコードはなく、タイプ固有のコードもないため、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呼び出しを使用して値を数回割り当てた場合、古い値の上にあるデータを初期化するだけです-itdoes n't毎回より多くのスタックスペースを割り当てます。
  • initobj:既に割り当てられたストレージの場所を使用し、データを消去します。これは、ローカル変数に割り当てるものを含む、すべてのパラメーターなしのコンストラクター呼び出しに使用されます。メソッド呼び出しの場合、中間ローカル変数が効果的に導入され、その値はinitobjによって消去されます。

このトピックがいかに複雑であるかを示しながら、少しだけ光を当てることを期待しています。 some概念的な意味では、newを呼び出すたびにスタック上のスペースが割り当てられます-しかし、これまで見てきたように、実際にはそうではありませんILレベルでも起こります。特定のケースを強調したいと思います。この方法を使用します。

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

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

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

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

298
Jon Skeet

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

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

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

38

コンパクトにまとめると、newは構造体の誤った呼び名です。newを呼び出すと、コンストラクターが呼び出されます。構造体の唯一の保管場所は、定義されている場所です。

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

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

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

12
Guvante

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

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

編集:私は彼らが常にスタックに行くと間違って答えました。これは 不正解 です。

5
Esteban Araya

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

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

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

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

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

4
user18579

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

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

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

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

詳細...

2
Sujit

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

1
bashmohandes

構造体はスタックに割り当てられます。役立つ説明を次に示します。

構造体

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

1
DaveK