演算子を使用してクラスのインスタンスを作成するとnew
、メモリはヒープ上に割り当てられます。 演算子を使用して構造体のインスタンスを作成すると、new
メモリはヒープ上またはスタック上のどこに割り当てられますか?
ベストアンサー1
わかりました。これをもっと明確に説明できるか見てみましょう。
まず、Ash の言う通りです。問題は値型変数がどこに割り当てられるかということではありません。これは別の問題であり、答えは単に「スタック上」ではありません。問題はそれよりも複雑です (C# 2 によってさらに複雑になっています)。トピックに関する記事要求があればさらに詳しく説明しますが、ここでは演算子だけを扱いますnew
。
第二に、これはすべて、どのレベルについて話しているのかによって大きく異なります。私は、コンパイラがソース コードに対して行う処理、つまりコンパイラが作成する IL について見ています。JIT コンパイラが、かなりの量の「論理的」割り当てを最適化するという点で、巧妙な処理を実行する可能性は十分にあります。
3 番目に、ジェネリックを無視しているのは、主に答えがわからないからであり、また、ジェネリックだと物事が複雑になりすぎるからでもあります。
最後に、これはすべて現在の実装に関するものです。C# 仕様では、これについてはあまり規定されていません。これは実質的に実装の詳細です。マネージ コード開発者は気にする必要はないと考える人もいます。私はそこまで言うかどうかわかりませんが、実際にすべてのローカル変数がヒープ上に存在する世界を想像する価値はあります。これは依然として仕様に準拠しています。
値型の演算子には 2 つの異なる状況がありますnew
。パラメーターなしのコンストラクター (例new Guid()
) またはパラメーター付きのコンストラクター (例new Guid(someString)
) のいずれかを呼び出すことができます。これらは、大幅に異なる IL を生成します。理由を理解するには、C# と CLI の仕様を比較する必要があります。C# によると、すべての値型にはパラメーターなしのコンストラクターがあります。CLI 仕様によると、値型にはパラメーターなしのコンストラクターはありません。(値型のコンストラクターをリフレクションで取得すると、パラメーターなしのコンストラクターは見つかりません。)
C# では、「値をゼロで初期化する」をコンストラクターとして扱うのが理にかなっています。これは、言語の一貫性を保つためです。つまり、常にnew(...)
コンストラクターを呼び出すと考えることができます。CLI では、呼び出す実際のコードはなく、型固有のコードもないため、これを別の方法で考えるのにも理にかなっています。
初期化後に値をどのように扱うかによっても違いが出てきます。
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());
}
}
無関係な部分 (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 レベルでも実際にはそうはなりません。ここでは、1 つの特定のケースについて取り上げたいと思います。次のメソッドを例に挙げます。
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 の値型の構築に関するブログ投稿詳細と適用されないケースについてはこちらをご覧ください。
この回答を書くことで多くのことを学びました。不明な点があれば、お気軽にお問い合わせください。