C#のクラスとオブジェクト:プライマリコンストラクター・required・初期化子

C#のクラスとオブジェクトの基本を.NET 10で検証し直しました。フィールドとプロパティ、C# 12プライマリコンストラクターの落とし穴(CS9124)、C# 11のrequired、オブジェクトのサイズ実測まで。

2026年11月改訂: この記事は2021年に韓国語で書いたクラスの講座を、日本語向けに書き直したものです。サンプルを .NET 10.0.14(C# 14)でビルドし直すと、動作と出力は同じでしたが、新しいプロジェクトでは warning CS8618 が3件出ます。また当時の出力にあった「続行するには何かキーを押してください」は Visual Studio が付けていた行で、dotnet run では表示されません。2021年以降に入ったプライマリコンストラクター(C# 12)・required(C# 11)・init(C# 9)と、オブジェクト1個が実際に何バイトになるかの実測を加えました。

C#のクラスをこれから学ぶ人、あるいは昔の書き方は知っているものの class Counter(string label) や required を見て戸惑った人向けの記事です。先に結論を書くと、クラスは設計図、オブジェクトは new で作った実体で、変数に入るのはオブジェクトそのものではなく参照です。新しく入った文法は、どれも「オブジェクトを作るときに値をどう確実に埋めるか」という問題を扱っています。

オブジェクトとクラス:状態と振る舞い

現実のものをプログラムに置き換えるときは、状態と振る舞いに分けて考えます。スマートフォンなら色やバッテリー残量が状態、電話をかける・切るが振る舞いです。C#では状態をフィールドやプロパティで、振る舞いをメソッドで表します。その組み合わせを定義したものがクラスで、クラスから実際に作った1つ1つがオブジェクト(インスタンス)です。たい焼きの型が1つあれば、たい焼きはいくつでも焼ける、という関係です。

必要な部分だけを残して残りを捨てることを抽象化と呼びます。ペットショップのアプリで犬を扱うなら、名前・性別・飼い主の名前と「吠える」くらいがあれば十分です。

2021年版にあった「オブジェクト=状態+振る舞い」の円形の図は外部資料の図だったため今回外し、この記事のために描いた図に差し替えました。

2021年のサンプルを .NET 10 で動かす

元の Dog のサンプルを、名前空間と using も含めてそのまま新しいコンソールプロジェクト(dotnet new console、net10.0)に入れました。要点は次のとおりです。

class Dog {
    public string name;
    public string gender;
    public string ownernames;
    public void Bark() {
        Console.WriteLine("{0}:ワン!", name);
    }
}
// Main
Dog dog = new Dog();
dog.name = "ポチ";
dog.gender = "オス";
dog.ownernames = "佐藤";
dog.Bark();
Console.WriteLine("名前: {0}, 性別: {1}, 飼い主: {2}", dog.name, dog.gender, dog.ownernames);
ポチ:ワン!
名前: ポチ, 性別: オス, 飼い主: 佐藤

出力は2021年と同じです(文字列は日本語に置き換えています)。ただしビルドすると警告が3件出ます。

Program.cs(7,23): warning CS8618: null 非許容の フィールド 'name' には、コンストラクターの終了時に null 以外の値が入っていなければなりません。'required' 修飾子を追加するか、フィールド を Null 許容として宣言することを検討してください。

.NET 6 からコンソールのテンプレートに <Nullable>enable</Nullable> が入ったためで、「null にならない string と宣言したのに値を入れていない」という指摘です。直し方は3つあります。既定値を入れる(= "")、null を許す(string?)、あるいはメッセージのとおり required を付ける。最後の方法は後で扱います。

new Dog() は「Dog の設計図からオブジェクトを1つ作り、コンストラクター Dog() を呼ぶ」という意味です。C# 9 からは左辺に型があれば Dog dog = new(); と省略できます。LangVersion を 8 にすると error CS8400: Feature 'target-typed object creation' is not available in C# 8.0 になることも確かめました。コンストラクターはコンストラクターとファイナライザーで詳しく扱います。

変数に入っているのは参照

クラスは参照型です。変数を別の変数に代入しても、オブジェクトはコピーされず、同じオブジェクトを指す参照がもう1つ増えるだけです。

var a = new Dog { Name = "Kan", Owner = "Min" };
var b = a;
b.Name = "Bori";
Console.WriteLine($"a.Name={a.Name} same={ReferenceEquals(a, b)}");
a.Name=Bori same=True

b を変えたのに a も変わりました。オブジェクトが1つしかないからです。代入で値そのものをコピーしたいなら構造体(struct)を使います。

図。フィールド Name・プロパティ Owner・メソッド Bark() を持つクラス Dog から、new と書いた2本の矢印が Name が Kan のオブジェクト1と、Name が Bori のオブジェクト2へ伸びる。オブジェクト1からは b = a と書いた点線の矢印が、変数 a と b が同じオブジェクトを指すことを示す箱へつながる。
クラス1つからオブジェクトをいくつでも作れる。new のたびにヒープ上に別のオブジェクトができ、変数どうしの代入でコピーされるのは参照だけ。

フィールドとプロパティの違い(概要)

2021年版は public フィールドを使っていましたが、いまのC#で外に公開するデータはほぼプロパティです。

class Dog
{
    public string Name = "";            // フィールド:変数そのもの
    public string? Owner { get; set; }  // プロパティ:get/set アクセサーを通る
    public void Bark() => Console.WriteLine($"{Name}: Woof!");
}

呼び出し側は dog.Name、dog.Owner と同じに見えますが、プロパティなら後から検証処理を足しても呼び出し側を壊さず、init で作成後に変更できないようにもでき、データバインディングやシリアライザーもプロパティを基準に動きます。内部の状態は private フィールド、外に見せる値はプロパティ、と覚えておけば十分です。自動実装プロパティや読み取り専用プロパティなどはプロパティの基本とプロパティの応用にまとめています。public や private はアクセス修飾子とthisで扱います。

2021年以降に増えたもの:プライマリコンストラクター・required・init

プライマリコンストラクター(C# 12)

クラス名の直後に引数を書く文法です。引数はクラス本体のどこからでも使えます。

class Counter(string label)
{
    private int count;
    public void Add() => count++;
    public override string ToString() => $"{label}={count}";
}
// var c = new Counter("clicks"); c.Add(); c.Add(); Console.WriteLine(c);
clicks=2

よくある誤解が2つあります。1つ目、label はプロパティではありません。 record と違って公開メンバーは作られないので、外から c.label を読むとこうなります。

error CS1061: 'Counter' に 'label' の定義が含まれておらず、型 'Counter' の最初の引数を受け付けるアクセス可能な拡張メソッド 'label' が見つかりませんでした。

2つ目、引数でプロパティを初期化しつつ、メソッドの中でも引数を使うと値が2つに分かれます。コンパイラーは警告を出しますが、実際に何が起きるかを確かめました。

class Dog(string name)
{
    public string Name { get; set; } = name;
    public void Bark() => Console.WriteLine($"{name}: Woof! (Name={Name})");
}
// var d = new Dog("Kan"); d.Name = "Bori"; d.Bark();
warning CS9124: パラメーター 'string name' は外側の型の状態にキャプチャされ、その値はフィールド、プロパティ、またはイベントの初期化にも使用されます。
Kan: Woof! (Name=Bori)

Name を "Bori" に変えたのに、Bark() は "Kan" のままです。キャプチャされた name と、プロパティ Name の値が別々に保存されているからです。後の実測のとおり、オブジェクトも8バイト大きくなります。ルールは単純で、プロパティを作ったら、メソッドの中でもプロパティを使うことです。

required メンバー(C# 11)と init(C# 9)

コンストラクターを書かずに「作るときに必ずこの値を入れて」と強制できます。

class User
{
    public required int Id { get; init; }
    public required string Email { get; init; }
    public string Nick { get; set; } = "(none)";
}
var u = new User { Id = 7, Email = "a@example.com" };   // 出力: 7 a@example.com (none)

Email を書き忘れたり、作成後に init プロパティを書き換えようとしたりするとビルドが止まります。

error CS9035: 必要なメンバー 'User.Email' は、オブジェクト初期化子または属性コンストラクターに設定する必要があります。
error CS8852: init 専用プロパティまたはインデクサー 'User.Id' を割り当てることができるのは、オブジェクト初期化子の中か、インスタンス コンストラクターまたは 'init' アクセサーの 'this' か 'base' 上のみです。

{ Id = 7, Email = ... } の部分がオブジェクト初期化子です。コンストラクターが終わった直後にプロパティへ値を入れる書き方で、プライマリコンストラクターとも組み合わせられます(new Pet("Kan") { Owner = "Min" })。

「C# N で追加」を自分で確かめる

記事に書いたバージョンが正しいか、-p:LangVersion を下げてビルドしました。

機能 試したバージョン 実際のエラー
クラスのプライマリコンストラクター 11 CS9058: Feature 'primary constructors' is not available in C# 11.0. Please use language version 12.0 or greater.
required メンバー 10 CS8936: … 'required members' … 11.0 or greater.
init アクセサー 8 CS8400: … 'init-only setters' … 9.0 or greater.
型を省略した new() 8 CS8400: … 'target-typed object creation' … 9.0 or greater.

値ごとにどれを選ぶかは、次の基準で考えています。

判断の図。最初の問い:呼び出し側が必ず値を渡すべきか。いいえなら、既定値付きの任意プロパティをオブジェクト初期化子で設定する。はいなら次の問い:名前で設定させるか、位置で渡させるか。名前なら required の init プロパティ、位置ならコンストラクターかプライマリコンストラクター。
新しいオブジェクトに値を入れる方法の選び方:required メンバー、コンストラクター(プライマリコンストラクター)、オブジェクト初期化子で設定する任意のプロパティ。

実測:オブジェクト1個は何バイトか

クラスのオブジェクトにはフィールドのほかにヘッダーが付きます。各クラスを100万個作って配列に入れ、GC.GetAllocatedBytesForCurrentThread() の増分を個数で割りました(Release ビルド、.NET 10.0.14、Apple Silicon Mac。3回とも同じ値)。

クラス 1個あたりの割り当て
new object() 24 B
空のクラス class Empty { } 24 B
int フィールド1つ 24 B
2021年版の Dog(string フィールド3つ) 40 B
Counter(string label) + int count 32 B
Dog(string name) + Name プロパティ、メソッドでも name を使用(CS9124) 32 B
Dog(string name) + Name プロパティのみ 24 B

64ビットの .NET ではオブジェクトヘッダーと型ポインターで16バイト、最小サイズが24バイトなので、空のクラスも int 1つのクラスも同じ24バイトです。参照型のフィールドは1つにつき8バイト増えます。CS9124 が出たクラスは同じ文字列への参照を2つ持つことになり、24バイトから32バイトになりました。1個なら誤差ですが、数十万個をメモリに載せるアプリならこの警告は無視しないほうがいいでしょう。

まとめ:どれをいつ使うか

状況 おすすめ
内部の状態 private フィールド
外に見せる値 プロパティ({ get; set; }、{ get; init; })
必須の値・名前で書くほうが読みやすい required + init
必須の値・数が少なく順番が自明 コンストラクター/プライマリコンストラクター
任意の値 既定値付きプロパティ+オブジェクト初期化子
プライマリコンストラクターの引数を外に見せたい プロパティを宣言し、メソッドでもプロパティを使う

このシリーズの続きとして、public・private などのアクセスレベルと this はアクセス修飾子とthis、オブジェクトが作られて片付けられる順番はコンストラクターとファイナライザーで扱います。メソッドそのもの(オーバーロード、ref、params)はC#のメソッドを参照してください。

検証環境:.NET SDK 10.0.403/ランタイム 10.0.14、C# 14(net10.0 の既定)、Apple Silicon Mac 上の macOS。

よくある質問

C# 12 のプライマリコンストラクターの引数はプロパティになる?

なりません。class Counter(string label) の label はクラスの中だけで見える引数で、外から c.label と書くと error CS1061 になります。record と違って公開プロパティは自動では作られないので、外に見せたいなら public string Label { get; } = label; と自分で宣言します。

required とコンストラクター、どちらを使うべき?

どちらも「必ず値を入れさせる」仕組みです。呼び出し側が名前で値を書くほうが読みやすいなら required + init プロパティ(C# 11)、値が少なく順番が自明ならコンストラクターかプライマリコンストラクターが向いています。required を書き忘れると error CS9035 でビルドが止まります。

昔のサンプルをビルドすると CS8618 警告が出るのはなぜ?

.NET 6 以降の新規プロジェクトは Nullable が既定で有効なので、初期化していない null 非許容の string フィールドごとに warning CS8618 が出ます。既定値を入れる(= "")、string? にする、required を付ける、のどれかで消えます。

  • #C#
  • #クラス
  • #オブジェクト
  • #プライマリコンストラクター
  • #required

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑