C#のnewを理解する――値型・参照型・スタック・ヒープの関係

広告

C#を学んでいると、クラスでは new を使うのに、int にも new が使えることに疑問を感じるかもしれません。

Person p = new Person();
int x = new int();

さらに「値型はスタック、参照型はヒープ」と覚えていると、話がうまくつながらなくなります。この記事では、new、値型・参照型、スタック・ヒープの関係を初学者向けに順番に整理します。

まず結論

newは、指定した型の値やオブジェクトを初期化して作るための構文です。

クラスで使うと、新しいオブジェクトが作られ、その参照が返されます。構造体で使うと、その構造体の値が初期化されます。

  • 値型:変数が値そのものを持つ
  • 参照型:変数がオブジェクトへの参照を持つ
  • スタックかヒープか:その値がどこに含まれているかで決まる

newは「ヒープにメモリを確保する命令」だけではない

初心者向けの説明では、「newを使うとヒープにメモリが確保される」と説明されることがあります。クラスについて考えるなら、大きくは間違っていません。

Person p = new Person();

概念的には、次の処理が行われます。

  1. Personオブジェクト用の領域を用意する
  2. フィールドを既定値で初期化する
  3. コンストラクターを実行する
  4. 作られたオブジェクトへの参照を返す

ただし、newの意味を「ヒープへのメモリ確保」だけで覚えると、次のコードを説明できません。

int x = new int();

intは値型です。それでもnewを使えるため、newの本質は単なるヒープ確保ではないと分かります。

C#のnew、値型・参照型、スタック・ヒープの関係を整理した図
newの意味と、値型・参照型、スタック・ヒープの関係

intもnewで初期化できる

intはC#での別名で、実体はSystem.Int32という構造体です。そのため、次の2つは同じ意味です。

int x = new int();
System.Int32 y = new System.Int32();

どちらも既定値の0で初期化されます。

Console.WriteLine(x); // 0

通常は意図が伝わりやすいように、次のように直接値を書きます。

int x = 0;

intの()に値は渡せる?

intには、値を受け取るコンストラクターはありません。したがって、次のコードはコンパイルエラーになります。

int x = new int(10); // コンパイルエラー

10で初期化したい場合は、素直に次のように書きます。

int x = 10;

構造体のnewは「初期化」と考える

自作の構造体を考えてみましょう。

struct Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

この構造体は、コンストラクターを使って次のように初期化できます。

Point p = new Point(10, 20);

このときpは、X = 10Y = 20という値を持ちます。構造体のnewは、値を正しい状態に初期化するためのものと考えると分かりやすいでしょう。

クラスのnewと構造体のnewの違い

クラスの場合

Person p = new Person();

新しいPersonオブジェクトが作られ、変数pには、そのオブジェクトへの参照が入ります。

参照型の変数pがPersonオブジェクトへの参照を持つことを示す図
参照型の変数は、オブジェクトそのものではなく、そのオブジェクトへの参照を持ちます。

構造体の場合

Point p = new Point(10, 20);

変数p自身が、XYの値を持ちます。別の場所にあるオブジェクトへの参照を持つわけではありません。

構造体変数pがX=10とY=20の値を直接持つことを示す図
構造体の変数は、フィールドの値を変数自身の中に持ちます。

構造体は必ずスタックに置かれる?

「値型はスタック、参照型はヒープ」という説明は覚えやすい一方、正確ではありません。構造体だから必ずスタックに置かれるわけではないからです。

重要なのは、その値がどこに含まれているかです。

ローカル変数の場合

void Sample()
{
    int x = 10;
    Point p = new Point(10, 20);
}

このようなローカル変数は、概念的にはメソッドのスタックフレーム内に置かれることが多いです。ただし、実際の配置はJITコンパイラーの最適化などにも左右されます。

クラスのフィールドの場合

class Person
{
    public Point Position;
}

Person person = new Person();

Personオブジェクトは通常、管理ヒープ上に作られます。値型のPositionは、そのオブジェクトの一部として中に直接含まれます。つまり、構造体であってもヒープ上に存在することがあります。

配列の要素の場合

int[] numbers = new int[3];

配列は参照型なので、その実体は通常ヒープ上に作られます。配列の3つのintも、配列の内部に直接格納されます。intだから必ずスタック、というわけではありません。

値型と参照型の本当の違い

値型と参照型は、スタックとヒープではなく、「値そのものを持つか、参照を持つか」で区別すると整理しやすくなります。

値型は値そのものを持つ

int a = 10;
int b = a;

b = 20;

aの値がbへコピーされるため、結果はa = 10b = 20です。2つの変数は別々の値を持ちます。

参照型は参照を持つ

class Person
{
    public string Name = "";
}

Person a = new Person();
a.Name = "田中";

Person b = a;
b.Name = "佐藤";

Console.WriteLine(a.Name); // 佐藤

abには、同じPersonオブジェクトへの参照が入っています。そのため、b経由で名前を変更すると、aから見ても名前は「佐藤」になります。

なぜ値型と参照型の両方にnewを使えるのか

C#では、「型の新しい値やオブジェクトを初期化して得る」という操作を、値型と参照型で共通の構文として表現できます。

Person person = new Person();
Point point = new Point();
int number = new int();

この統一された書き方は、ジェネリックでも役立ちます。

T Create<T>() where T : new()
{
    return new T();
}

Tが制約を満たせば、クラスでも構造体でもnew T()という同じ形で生成できます。

最後に整理

項目覚え方
new型の値やオブジェクトを初期化して作る構文
クラスオブジェクトを作り、その参照を返す
構造体値を初期化し、変数が値そのものを持つ
int x = new int()既定値の0で初期化する
値型値そのものを持つ
参照型オブジェクトへの参照を持つ
スタック/ヒープ型だけでなく、その値がどこに含まれるかで考える

まとめ

C#のnewは、「ヒープにメモリを確保する命令」とだけ覚えるのではなく、「型の値やオブジェクトを初期化して作る構文」と理解する方が自然です。

また、「値型か参照型か」と「スタックにあるかヒープにあるか」は別の話です。特に大切なのは、次の一言です。

値型は値を持つ。参照型は参照を持つ。

この考え方を軸にすると、int、構造体、クラス、配列、そしてnewの関係を整理しやすくなります。

訪問数 7 回, 今日の訪問数 7回

広告