1. ホーム
  2. c#

[解決済み】構造体の "new "は、ヒープやスタックに割り当てるのですか?

2022-04-15 06:22:30

質問

を使用してクラスのインスタンスを作成する場合、そのインスタンスは new 演算子を使用すると、ヒープ上にメモリが確保されます。構造体のインスタンスを new 演算子は、ヒープやスタックのどこにメモリを確保するのですか?

どのように解決するのですか?

では、もう少し分かりやすく説明しましょう。

まず、アッシュの言う通り、問題は ではなく を使用することができます。 変数 が割り当てられています。これは別の問題で、答えは単に「スタック上」だけではありません。C#2では、さらに複雑になっています。私は このトピックに関する記事 のみであり、要望があればさらに詳しく説明しますが、ここでは new 演算子を使用します。

第二に、これらのすべては、あなたがどのレベルについて話しているのかに本当に依存します。私は、コンパイラがソースコードに対して何をするのか、ILを作成するという観点から見ています。JITコンパイラは、多くの論理的アロケーションを最適化するような巧妙なことを行う可能性があります。

第三に、ジェネリックを無視するのは、実は答えを知らないからであり、物事を複雑にしすぎるからでもある。

最後に、これらはすべて、現在の実装での話です。C#の仕様ではあまり規定されておらず、事実上、実装のディテールとなっています。マネージドコードの開発者は本当に気にする必要はないと考える人もいます。しかし、実際にすべてのローカル変数がヒープ上に存在する世界を想像してみると、やはり仕様に準拠することになるでしょう。


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

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

また、初期化した後の値をどうするかも違います。に使われるILは

Guid localVariable = new Guid(someString);

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

myInstanceOrStaticVariable = new Guid(someString);

また、メソッド呼び出しの引数など、中間値として使われる場合は、また少し事情が違ってきます。これらの違いを示すために、ここに短いテストプログラムがあります。これは、スタティック変数とインスタンス変数の違いを示すものではありません。 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つのスタックが確保されます。 new しかし、実際には(この特定のコードでは)スタックは一度だけ割り当てられ、その後同じ記憶場所が再利用されます。

EDIT: 念のため言っておきますが、これは一部のケースにのみ当てはまります。 guid は表示されません。 Guid コンストラクタが例外を投げるので、C#コンパイラは同じスタックスロットを再利用できるのです。Eric Lippertの ブログ記事 の詳細と、その事例として はありません。 が適用されます。

この回答を書くにあたり、多くのことを学びました。もし不明な点があれば、説明を求めてください