演算子を使用してクラスのインスタンスを作成するとnew
、メモリがヒープに割り当てられます。演算子を使用して構造体のインスタンスを作成するとnew
、ヒープまたはスタックのどこにメモリが割り当てられますか?
8 に答える
わかりました、これをもっと明確にすることができるかどうか見てみましょう。
まず、Ash は正しいです。問題は、値型変数がどこに割り当てられるかではありません。それは別の質問です。答えは「スタック上」だけではありません。それよりも複雑です (C# 2 ではさらに複雑になります)。このトピックに関する記事があり、要求があれば詳しく説明しますが、オペレーターだけを扱いましょう。new
第二に、これはすべて、話しているレベルによって異なります。コンパイラが作成する IL に関して、コンパイラがソース コードで何をするかを調べています。多くの「論理的」割り当てを最適化するという点で、JIT コンパイラーが巧妙なことを行う可能性は十分にあります。
第三に、私はジェネリックを無視しています。その主な理由は、実際には答えがわからないためであり、部分的には物事が複雑になりすぎるためです。
最後に、これはすべて現在の実装に基づいています。C# 仕様では、これについてはあまり指定されていません。事実上、実装の詳細です。マネージ コードの開発者は気にする必要はないと考える人もいます。そこまで行くかどうかはわかりませんが、実際にはすべてのローカル変数がヒープ上に存在する世界を想像する価値があります-それでも仕様に準拠します.
値型の演算子には 2 つの異なる状況があります。パラメーターなしのコンストラクター ( など) またはパラメーター付きのコンストラクター ( など)new
を呼び出すことができます。これらは、大幅に異なる IL を生成します。その理由を理解するには、C# と CLI の仕様を比較する必要があります。C# によると、すべての値型にはパラメーターなしのコンストラクターがあります。CLI 仕様によると、パラメーターのないコンストラクターを持つ値型はありません。(しばらくリフレクションを使用して値型のコンストラクターを取得します。パラメーターのないものは見つかりません。)new Guid()
new Guid(someString)
C# が「値をゼロで初期化する」をコンストラクターとして扱うのは理にかなっています。これは、言語の一貫性を保つためです。常にnew(...)
コンストラクターを呼び出すと考えることができます。呼び出す実際のコードはなく、確かに型固有のコードもないため、CLI がそれを別の方法で考えるのは理にかなっています。
また、初期化後に値をどうするかにも違いがあります。に使用される IL
Guid localVariable = new Guid(someString);
以下に使用される IL とは異なります。
myInstanceOrStaticVariable = new Guid(someString);
さらに、値がメソッド呼び出しの引数などの中間値として使用される場合、状況は少し異なります。これらすべての違いを示すために、ここに短いテスト プログラムを示します。静的変数とインスタンス変数の違いは示されていません。IL は と の間stfld
で異なりますがstsfld
、それだけです。
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());
}
}
無関係なビット (nop など) を除いた、クラスの 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 つ、3 つのnew
呼び出しごとに 1 つ) がありますが、実際には (その特定のコードに対して) スタックは 1 回だけ割り当てられ、同じストレージの場所が再利用されます。
編集: 明確にするために、これはいくつかの場合にのみ当てはまります...特に、コンストラクターが例外をスローしたguid
場合、の値は表示されませんGuid
。これが、C# コンパイラーが同じスタックスロットを再利用できる理由です。詳細および適用されないケースについては、値型の構築に関するEric Lippert のブログ投稿を参照してください。
私はこの回答を書くことで多くのことを学びました - 不明な点がある場合は、説明を求めてください!
構造体のフィールドを含むメモリは、状況に応じてスタックまたはヒープに割り当てることができます。struct-type 変数が、一部の匿名デリゲートまたは反復子クラスによってキャプチャされないローカル変数またはパラメーターである場合、スタックに割り当てられます。変数が何らかのクラスの一部である場合、その変数はヒープ上のクラス内に割り当てられます。
構造体がヒープに割り当てられている場合、メモリを割り当てるために実際に new 演算子を呼び出す必要はありません。唯一の目的は、コンストラクターにあるものに従ってフィールド値を設定することです。コンストラクターが呼び出されない場合、すべてのフィールドはデフォルト値 (0 または null) を取得します。
スタックに割り当てられた構造体についても同様ですが、C# ではすべてのローカル変数を使用する前に何らかの値を設定する必要があるため、カスタム コンストラクターまたは既定のコンストラクターのいずれかを呼び出す必要があります (パラメーターを取らないコンストラクターは、常に使用できます)。構造体)。
簡潔に言うと、new は構造体の誤称であり、new を呼び出すと単純にコンストラクターが呼び出されます。構造体の唯一の格納場所は、それが定義されている場所です。
メンバー変数の場合は、定義されているものに直接格納され、ローカル変数またはパラメーターの場合はスタックに格納されます。
これを、参照がヒープのどこかを指しているのに対し、構造体全体が格納された場所ならどこでも参照を持つクラスとは対照的です。(メンバー内、ローカル/スタック上のパラメーター)
クラスと構造体の間に実際の区別がない C++ を少し調べると役立つ場合があります。(言語にも同様の名前がありますが、それらはデフォルトのアクセシビリティを参照しているだけです) new を呼び出すと、ヒープの場所へのポインターが取得されますが、ポインター以外の参照がある場合は、スタックまたはスタックに直接格納されます。他のオブジェクト内では、C# の ala 構造体です。
私はおそらくここで何かを見逃していますが、なぜ割り当てを気にするのでしょうか?
値型は値によって渡される ;) したがって、定義されている場所とは異なるスコープで変更することはできません。値を変更できるようにするには、[ref] キーワードを追加する必要があります。
参照型は参照によって渡され、変更することができます。
もちろん、最も人気のある不変の参照型の文字列があります。
配列のレイアウト/初期化: 値型 -> ゼロ メモリ [name,zip][name,zip] 参照型 -> ゼロ メモリ -> null [ref][ref]
class
orstruct
宣言は、実行時にインスタンスまたはオブジェクトを作成するために使用される設計図のようなものです。class
または呼び出される Personを定義する場合struct
、Person は型の名前です。Person 型の変数 p を宣言して初期化する場合、p は Person のオブジェクトまたはインスタンスであると言われます。同じ Person タイプの複数のインスタンスを作成でき、各インスタンスはそのproperties
とに異なる値を持つことができますfields
。
Aclass
は参照型です。のオブジェクトclass
が作成されると、オブジェクトが割り当てられる変数は、そのメモリへの参照のみを保持します。オブジェクト参照が新しい変数に割り当てられると、新しい変数は元のオブジェクトを参照します。どちらの変数も同じデータを参照するため、一方の変数で行われた変更は他方の変数に反映されます。
Astruct
は値型です。がstruct
作成されると、 が割り当てられた変数がstruct
構造体の実際のデータを保持します。がstruct
新しい変数に割り当てられると、コピーされます。したがって、新しい変数と元の変数には、同じデータの 2 つの別個のコピーが含まれます。1 つのコピーに加えられた変更は、他のコピーには影響しません。
一般に、より複雑な動作や、オブジェクトの作成classes
後に変更する予定のデータをモデル化するために使用されます。の作成後に変更する予定のないデータを主に含む小さなデータ構造に最適です。class
Structs
struct
構造体はスタックに割り当てられます。役立つ説明は次のとおりです。
さらに、.NET 内でインスタンス化されたクラスは、ヒープまたは .NET の予約済みメモリ空間にメモリを割り当てます。一方、スタックでの割り当てにより、構造体はインスタンス化されたときに効率が向上します。さらに、構造体内でのパラメーターの受け渡しは、値によって行われることに注意してください。
値型と見なされるほとんどの構造体はスタックに割り当てられ、オブジェクトはヒープに割り当てられ、オブジェクト参照 (ポインター) はスタックに割り当てられます。