Menu

C#のコンストラクター:既定、引数付き、this()とbase()の連鎖

C#のコンストラクターの仕組みを解説します。暗黙の既定のコンストラクターと、それが消える場合、引数付きとオーバーロードされたコンストラクター、this(...)とbase(...)による連鎖、実行される順序、静的とprivateなコンストラクター、そしてC# 12のプライマリコンストラクターを扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

コンストラクターは、new でオブジェクトを作るときに実行されるコードです。クラスと同じ名前を持ち、戻り値の型はなく、その役割は、誰かが使う前に新しいオブジェクトを有効な状態にすることです。

出力:

Mug: 8.50
Rejected: price

コンストラクターが負の価格を拒否するので、負の価格を持つ Product は存在しえません。この保証こそ、外からフィールドを設定する代わりにコンストラクターを書く主な理由です。

既定のコンストラクターと、それが消える場合

クラスがコンストラクターを1つも宣言していないと、コンパイラは、フィールドの初期化子を実行する以外に何もしない公開の引数なしのコンストラクターを用意します。だから、フィールドしかないクラスでも new BankAccount() が動きます。

何か1つでもコンストラクターを宣言した時点で、その暗黙のものは生成されなくなります。

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

シリアライザー、ORM、ジェネリックの new() 制約が引数なしのコンストラクターを必要とするときに、これに驚かされます。両方が必要なら、引数なしのものを自分で宣言します。

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

コンストラクターは public、internal、protected、private のどれにでもできます。そのアクセスによって、誰がそれでオブジェクトを作れるかが決まります。

オーバーロードとthis(...)による連鎖

クラスは、引数リストの異なる複数のコンストラクターを持てます。同じ代入をそれぞれにコピーしないように、連鎖させます。: this(...) は、現在の本体が実行される前に、同じクラスの別のコンストラクターを呼びます。

出力:

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

連鎖は最も完全なコンストラクターを最初に実行し、呼び出したものへと戻っていきます。本当の処理(検証、代入)は1つのコンストラクターに置き、他のものには既定値を与えるだけにします。

省略可能な引数も代替手段になります。public Pizza(string size = "medium", string crust = "classic", int toppings = 0) なら、1つのコンストラクターで3つの呼び出しすべてをまかなえます。それでも、短いほうのコンストラクターが何かを計算する必要がある場合や、既定値を変えても呼び出し側の再コンパイルを必要としたくない場合(省略可能な引数の既定値はコンパイル時に呼び出し側のコードにコピーされます)は、連鎖のほうが適しています。

base(...)で基底クラスのコンストラクターを呼ぶ

派生クラスはコンストラクターを継承しません。派生クラスのどのコンストラクターも、最初に基底クラスのコンストラクターを実行しなければなりません。何も書かなければ、コンパイラは基底クラスの引数なしのコンストラクターの呼び出しを挿入します。基底クラスにそれがなければ、: base(...) でどれかを選ばなければなりません。

すべてが実行される順序は、一度見ておく価値があります。

出力:

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

フィールドの初期化子が最初に、派生クラスが基底クラスより先に実行され、その後にコンストラクターの本体が基底から下へと実行されます。そのため、Truck の本体が実行される時点で、Plate はすでに設定されています。この順序の落とし穴が1つあります。基底のコンストラクターが、Truck がオーバーライドしている virtual メソッドを呼ぶと、オーバーライドは Truck のコンストラクターの本体より前に実行され、Axles はまだ 0 のままです。コンストラクターから仮想メソッドを呼ぶのは避けます。

静的コンストラクター

静的コンストラクターは、オブジェクトではなく型を初期化します。引数もアクセス修飾子もなく、ランタイムは型が最初に使われる直前にちょうど1回それを呼びます。

出力:

Program started
Loading tax table (runs once)
123.00
119.00

ランタイムは静的コンストラクターをスレッドセーフにします。複数のスレッドが同時に型に触れても、実行されるのは1回です。例外を投げると、その後の型の使用はすべて、プロセスが存在する間ずっと TypeInitializationException を投げるので、単純に保ち、失敗しうるI/Oは入れないようにします。

コンストラクターとオブジェクト初期化子

オブジェクト初期化子 new Pizza("large") { Toppings = 3 } は2つ目のコンストラクターではありません。コンパイラはそれを「コンストラクターを実行してから、これらのメンバーに代入する」に変換します。それぞれを得意なことに使います。

  • コンストラクターの引数は、オブジェクトがそれなしでは有効にならない値のために使います。すべての呼び出し側がそれを渡さなければならず、コンストラクターはそれを確認できます。
  • 初期化子は、妥当な既定値を持つ省略可能な設定のために使います。
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

C# 11以降、required を付けたプロパティは初期化子で設定しなければならないので、初期化子の構文にもコンストラクターの保証の一部が加わりました。これはプロパティのページで扱います。

privateなコンストラクター

private なコンストラクターは、クラス自身しかインスタンスを作れないことを意味します。これを使うパターンは2つあります。クラスが作成を制御し、キャッシュしたオブジェクトや null を返せる静的なファクトリーメソッドと、シングルトンです。

出力:

21
100

クラスの外での new Temperature(5m) のコンパイルエラーはCS0122で、アクセス修飾子がprivateなメンバーに対して出すものと同じです。名前付きのファクトリーメソッドは、実際の制約を解決します。2つのコンストラクターがどちらも1つの decimal を受け取ることはできませんが、FromCelsius と FromFahrenheit ならできます。静的メンバーしかないクラス(ユーティリティクラス)は、コンストラクターを隠す代わりに static として宣言すべきです。

式形式のコンストラクター

本体が1つの文であるコンストラクターには => を使えます。

public Product(string name) => Name = name;

タプルを使えば、1行で複数のフィールドに代入することもできます:public Point(int x, int y) => (X, Y) = (x, y);。

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

C# 12では、クラスや構造体が、型そのものにコンストラクターの引数を宣言できるようになりました。引数はすべてのメンバーでスコープ内にあります。

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

レコードとの違いで驚かされる点が2つあります。クラスのプライマリコンストラクターの引数は公開のプロパティにならないので、自分で公開します。また、メンバーの中で使われた引数は隠しフィールドにキャプチャされ、変更可能なままなので、メソッドの中で name = "x"; と書いてもコンパイルできます。プライマリコンストラクターが最も役立つのは依存性の注入で、引数はクラスが呼び出すだけのサービスです。

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

よくある間違い

  • 引数なしのコンストラクターを失う。 引数付きのコンストラクターを追加すると、暗黙のものが消えます(すべての new X() でCS7036)。
  • 基底クラスに引数なしのコンストラクターがないのに : base(...) を忘れる。 コンパイラは暗黙の呼び出しを挿入できず、基底のコンストラクターの必須の引数に対応する引数がないと報告します。
  • コンストラクターの中で仮想メソッドを呼ぶ。 オーバーライドが派生クラスのコンストラクターの本体より前に実行されます。
  • コンストラクターで重い処理をする。 ネットワークの呼び出し、ファイルの読み込みなど遅いものは、オブジェクトを高コストにし、例外の処理を難しくします。そうした処理には、静的なファクトリーメソッドか非同期の初期化メソッドを使います。
  • 戻り値の型を書く。 public void Product() は Product という名前の普通のメソッドで、メンバーは外側の型と同じ名前を持てないので、コンパイラが拒否します(CS0542)。

よくある質問

C#のコンストラクターとは何ですか?

コンストラクターは、new でオブジェクトが作られるときに実行される特別なメソッドです。クラスと同じ名前を持ち、戻り値の型はありません:public Product(string name) { Name = name; }。その役割は新しいオブジェクトを有効な状態にすることで、通常は引数からフィールドに代入し、不正な入力を拒否します。

C#は既定のコンストラクターを自動で作りますか?

クラスがコンストラクターを1つも宣言していない場合だけです。その場合、コンパイラは、すべてのフィールドを初期化子の値か既定値のままにする公開の引数なしのコンストラクターを追加します。名前を受け取るものなど、何か1つでもコンストラクターを書くと、暗黙のものは消え、new Product() はコンパイルできなくなります(エラーCS7036)。それでも必要なら、public Product() { } を自分で宣言します。

C#であるコンストラクターから別のコンストラクターを呼ぶには?

コンストラクターの引数リストの後に : this(...) を書きます:public Product(string name) : this(name, 0m) { }。呼び出し先のコンストラクターが先に実行され、その後に呼んだほうの本体が実行されます。基底クラスのコンストラクターを呼ぶには、同じように : base(...) を使います。

C#の静的コンストラクターとは何ですか?

static を付けた、引数もアクセス修飾子もないコンストラクターで、最初のインスタンスが作られる前か、最初の静的メンバーが使われる前に、型ごとに1回だけ実行されます。1行の初期化子では足りない静的フィールドの初期化に使います。自分で呼び出すことはできず、例外を投げると、その型はプログラムの残りの間ずっと使えなくなります(TypeInitializationException)。

コンストラクターとオブジェクト初期化子のどちらを使うべきですか?

オブジェクトがそれなしでは有効にならない値には、コンストラクターの引数を使います。コンパイラがすべての呼び出し側にそれを渡すよう強制し、コンストラクターが値を確認できるからです。省略可能な設定にはオブジェクト初期化子(new Product("Mug") { Color = "blue" })を使います。初期化子はコンストラクターが終わった後に実行されます。

C# 12のプライマリコンストラクターとは何ですか?

クラスの宣言そのものに書く引数で、class Customer(string name, int id) { ... } のように書き、クラスのすべてのメンバーで使えます。レコードの位置指定の引数と違い、公開のプロパティにはなりません。public string Name => name; や public string Name { get; } = name; で明示的に公開します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める