コンストラクタは C# クラスの初期化処理を担う特別なメソッドです。「基本的な使い方は知っている」という方でも、静的コンストラクタ・プライマリコンストラクタ(C# 12)・継承時の : base・プライベートコンストラクタを使ったファクトリパターンまで理解すると、設計の幅が格段に広がります。
本記事ではコンストラクタの基礎から実践的な設計パターン・よくある落とし穴まで体系的に解説します。
コンストラクタの基本構文
コンストラクタはクラス名と同じ名前を持ち、戻り値型を書かない特別なメソッドです。new でオブジェクトを生成したときに自動的に呼び出されます。
class Person
{
// フィールド
private readonly string _name;
private readonly int _age;
// コンストラクタ(クラス名と同名・戻り値型なし)
public Person(string name, int age)
{
// 引数を検証してフィールドに代入
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("名前は空にできません", nameof(name));
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age));
_name = name;
_age = age;
}
public string Name => _name;
public int Age => _age;
public override string ToString() => $"{_name}({_age}歳)";
}
var p = new Person("Taro", 25);
Console.WriteLine(p); // Taro(25歳)
① クラス名と同じ名前にする
② 戻り値型(
void を含む)を書かない③
new によるオブジェクト生成時に自動的に呼ばれる
デフォルトコンストラクタの仕組み
コンストラクタを1つも定義しないクラスには、コンパイラが引数なしの「デフォルトコンストラクタ」を自動生成します。しかし引数ありのコンストラクタを1つでも書くと、自動生成は行われません。
// コンストラクタを定義しない → デフォルトコンストラクタが自動生成される
class Config
{
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 8080;
}
var c = new Config(); // 引数なしで生成OK
Console.WriteLine(c.Host); // localhost
// ─────────────────────────────────
// 引数ありコンストラクタを定義すると
// デフォルトコンストラクタは自動生成されなくなる
class Server
{
public string Host { get; }
public int Port { get; }
public Server(string host, int port)
{
Host = host;
Port = port;
}
}
// var s = new Server(); // ← コンパイルエラー!
var s = new Server("example.com", 443); // 引数必須
new MyClass()」が使えなくなるのはよくある失敗です。引数なし生成も残したい場合は public MyClass() { } を明示的に追加してください。オーバーロードと : this 委譲
コンストラクタは引数の数や型が異なるものを複数定義(オーバーロード)できます。重複した初期化処理は : this(…) で別のコンストラクタに委譲して DRY に保ちます。
class HttpRequest
{
public string Method { get; }
public string Url { get; }
public int Timeout { get; }
// 最も完全なコンストラクタ(すべての初期化処理はここに集約)
public HttpRequest(string method, string url, int timeout)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URLは必須です");
Method = method.ToUpperInvariant();
Url = url;
Timeout = timeout;
}
// タイムアウトを省略したい場合は既定値 30 秒を渡して委譲
public HttpRequest(string method, string url)
: this(method, url, 30) { }
// GET のみ、URLだけでよい場合
public HttpRequest(string url)
: this("GET", url, 30) { }
}
var r1 = new HttpRequest("https://example.com");
var r2 = new HttpRequest("POST", "https://example.com/api");
var r3 = new HttpRequest("PUT", "https://example.com/api/1", 60);
Console.WriteLine(r1.Method); // GET
Console.WriteLine(r2.Timeout); // 30
Console.WriteLine(r3.Timeout); // 60
フィールド初期化子とコンストラクタの実行順序
C# では「フィールド初期化子(= 値)」がコンストラクタ本体より先に実行されます。: this 委譲がある場合は委譲先が先に呼ばれます。
class Demo
{
// ① フィールド初期化子が最初に実行
private int _a = Log("フィールド _a 初期化");
private int _b = Log("フィールド _b 初期化");
// ② デフォルトコンストラクタが : this で③を呼ぶ
public Demo() : this(0)
{
Console.WriteLine("② Demo() 本体");
}
// ③ 委譲先コンストラクタ
public Demo(int value)
{
Console.WriteLine($"③ Demo(int={value}) 本体");
}
private static int Log(string msg)
{
Console.WriteLine($"① {msg}");
return 0;
}
}
var d = new Demo();
// 出力:
// ① フィールド _a 初期化
// ① フィールド _b 初期化
// ③ Demo(int=0) 本体 ← 委譲先が先に実行される
// ② Demo() 本体
| 順序 | 処理 | 備考 |
|---|---|---|
| ① | フィールド初期化子 | 宣言した順に上から実行 |
| ② | : this / : base の委譲先コンストラクタ本体 | 委譲チェーンの最奥から実行 |
| ③ | 自分自身のコンストラクタ本体 | 最後に実行 |
継承と : base コンストラクタ
子クラスのコンストラクタは、: base(…) で親クラスのコンストラクタを呼び出せます。省略すると親の引数なしコンストラクタが自動的に呼ばれますが、親に引数なしコンストラクタがない場合はコンパイルエラーになります。
// 親クラス
class Animal
{
public string Name { get; }
public string Species { get; }
public Animal(string name, string species)
{
Name = name;
Species = species;
}
}
// 子クラス
class Dog : Animal
{
public string Breed { get; }
// : base で親クラスのコンストラクタを呼ぶ
public Dog(string name, string breed)
: base(name, "Canis lupus familiaris") // 種は固定
{
Breed = breed;
}
}
class GoldenRetriever : Dog
{
public GoldenRetriever(string name)
: base(name, "Golden Retriever") { } // 品種は固定
}
var g = new GoldenRetriever("Buddy");
Console.WriteLine(g.Name); // Buddy
Console.WriteLine(g.Species); // Canis lupus familiaris
Console.WriteLine(g.Breed); // Golden Retriever
Animal() → Dog() → GoldenRetriever() の順です。base キーワードの詳細は継承とオーバーライドの基本を参照してください。静的コンストラクタ(static constructor)
静的コンストラクタは static を付けた引数なしのコンストラクタです。クラスが初めてアクセスされた時点で自動的に1度だけ実行され、静的フィールドの初期化に使います。
class AppSettings
{
// 静的フィールド
public static readonly string Environment;
public static readonly string Version;
// 静的コンストラクタ(アクセス修飾子なし・引数なし)
static AppSettings()
{
Environment = System.Environment.GetEnvironmentVariable("APP_ENV") ?? "development";
Version = System.Reflection.Assembly.GetExecutingAssembly()
.GetName().Version?.ToString() ?? "0.0.0";
Console.WriteLine("静的コンストラクタが実行されました");
}
}
// 初めてアクセスしたときに静的コンストラクタが実行される
Console.WriteLine(AppSettings.Environment); // 静的コンストラクタが実行されました → development
Console.WriteLine(AppSettings.Version); // 0.0.0(環境による)
Console.WriteLine(AppSettings.Environment); // 2回目以降は静的コンストラクタが呼ばれない
| 項目 | 仕様 | 補足 |
|---|---|---|
| 呼び出し回数 | クラスが最初にアクセスされたとき1回のみ | 手動呼び出し不可 |
| 引数 | なし(引数を持てない) | アクセス修飾子も付けられない |
| アクセス修飾子 | 付けられない | 常に private 扱い |
| 例外 | 発生すると TypeInitializationException でラップされる | 以降そのクラスは使用不可 |
| 用途 | 静的フィールドの複雑な初期化 | DB接続・設定読み込み・キャッシュ構築など |
プライマリコンストラクタ(C# 12以降)
C# 12 からプライマリコンストラクタがクラスと構造体に導入されました。クラス宣言の直後にパラメーターリストを書くことで、コンストラクタ本体を省略できます。
// 従来の書き方
class OldPoint
{
public int X { get; }
public int Y { get; }
public OldPoint(int x, int y)
{
X = x;
Y = y;
}
}
// C# 12 プライマリコンストラクタ
class Point(int x, int y)
{
// パラメーターをプロパティに割り当て
public int X { get; } = x;
public int Y { get; } = y;
// メソッド内でもパラメーターを参照できる
public double DistanceTo(Point other)
=> Math.Sqrt(Math.Pow(x - other.X, 2) + Math.Pow(y - other.Y, 2));
public override string ToString() => $"({x}, {y})";
}
var p = new Point(3, 4);
Console.WriteLine(p); // (3, 4)
Console.WriteLine(p.DistanceTo(new Point(0, 0))); // 5
// ASP.NET Core でよく使うパターン
// 従来
class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<Order> GetAsync(int id)
{
_logger.LogInformation("注文 {Id} を取得します", id);
return await _repo.FindAsync(id);
}
}
// C# 12 プライマリコンストラクタ(ボイラープレートが激減)
class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order> GetAsync(int id)
{
logger.LogInformation("注文 {Id} を取得します", id);
return await repo.FindAsync(id);
}
}
x・y)はクラス全体のスコープで参照できますが、フィールドとして自動生成されるわけではありません。プロパティへの割り当て(= x)を省略すると、メソッド本体での参照のみ有効です(GCに注意)。また入力値の検証はコンストラクタ本体が書けないため、プロパティの init アクセサや別メソッドに実装してください。プライベートコンストラクタの活用
コンストラクタを private にすると外部から直接 new ができなくなります。これを活用してファクトリメソッドパターンやシングルトンパターンを実装します。
ファクトリメソッドパターン
class Temperature
{
private readonly double _celsius;
// 外部から直接 new を禁止
private Temperature(double celsius) => _celsius = celsius;
// ファクトリメソッドで「意味のある名前」から生成
public static Temperature FromCelsius(double c) => new(c);
public static Temperature FromFahrenheit(double f) => new((f - 32) * 5 / 9);
public static Temperature FromKelvin(double k) => new(k - 273.15);
public double Celsius => _celsius;
public double Fahrenheit => _celsius * 9 / 5 + 32;
public double Kelvin => _celsius + 273.15;
public override string ToString() => $"{_celsius:F1}°C";
}
var t1 = Temperature.FromCelsius(100);
var t2 = Temperature.FromFahrenheit(32);
var t3 = Temperature.FromKelvin(373.15);
Console.WriteLine(t1); // 100.0°C
Console.WriteLine(t2); // 0.0°C
Console.WriteLine(t3); // 100.0°C
シングルトンパターン
class AppLogger
{
// Lazy<T> によるスレッドセーフな遅延初期化
private static readonly Lazy<AppLogger> _instance
= new(() => new AppLogger());
// private コンストラクタ
private AppLogger()
{
Console.WriteLine("AppLogger 初期化");
}
public static AppLogger Instance => _instance.Value;
public void Log(string message)
=> Console.WriteLine($"[LOG] {message}");
}
AppLogger.Instance.Log("起動完了"); // AppLogger 初期化 → [LOG] 起動完了
AppLogger.Instance.Log("処理開始"); // [LOG] 処理開始(初期化は1回のみ)
コンストラクタ設計の原則
コンストラクタを正しく設計するための指針です。これらを守ることで「生成した瞬間から使える・壊れていない」オブジェクトを保証できます。
| 原則 | 理由 | 具体的な対処 |
|---|---|---|
| バリデーションはコンストラクタで | 不正な状態のオブジェクトを生成させない | 引数を検証して例外をスロー。生成後に「無効かどうか」を毎回チェックしなくて済む |
| コンストラクタは薄くする | I/O・非同期・重い計算を行わない | DBアクセス・ファイル読み込みはファクトリメソッドや別の Initialize() に切り出す |
| 仮想メソッドを呼ばない | 継承クラスが未初期化のまま呼ばれる危険がある | コンストラクタ内で virtual/abstract メソッドを呼ぶと子クラスの初期化より先に実行される |
| readonly フィールドと組み合わせる | コンストラクタで設定後は変更不可にする | private readonly T _field; でイミュータブルを保証 |
| オーバーロードは: this に集約 | 初期化ロジックの重複を防ぐ | 検証・変換は1か所にまとめ、他のオーバーロードは委譲のみ |
よくある落とし穴と注意点
コンストラクタ内で仮想メソッドを呼ぶ
class Base
{
public Base()
{
// NG: 仮想メソッドを呼ぶと Derived が未初期化のまま実行される
Initialize(); // Derived.Initialize() が呼ばれるが _value はまだ 0
}
protected virtual void Initialize()
=> Console.WriteLine("Base.Initialize");
}
class Derived : Base
{
private readonly int _value;
public Derived(int value)
: base() // ← base() が先に走り、Initialize() が呼ばれる時点で _value = 0
{
_value = value; // コンストラクタ本体が動くのはここ(遅い)
}
protected override void Initialize()
=> Console.WriteLine($"Derived.Initialize: {_value}"); // _value は 0 になる!
}
var d = new Derived(42);
// 出力: Derived.Initialize: 0 ← 期待値は 42 だが未初期化!
virtual・abstract メソッドや protected なオーバーライド対象メソッドを呼ぶのは設計上のバグになりやすいです。代わりにファクトリメソッドで生成後に初期化処理を呼ぶパターンを使いましょう。静的コンストラクタで例外が発生する
静的コンストラクタ内で例外がスローされると TypeInitializationException でラップされ、以降そのクラスは一切使用できなくなります(アプリ再起動まで)。外部リソース(環境変数・ファイルなど)にアクセスする場合は、存在しない場合のデフォルト値を用意するか、Lazy<T> を使って遅延初期化にしましょう。
引数ありコンストラクタを定義してデフォルト生成が使えなくなる
引数ありコンストラクタを1つでも定義した瞬間、コンパイラが自動生成するデフォルトコンストラクタが消えます。シリアライザ(System.Text.Json・EF Core など)は引数なしコンストラクタを要求することがあるため、気づかずに実行時エラーになるケースがあります。シリアライズが必要なクラスには public MyClass() { } を明示的に追加するか、コンストラクタに [JsonConstructor] 属性を付けてください。
: this / : base の呼び出し順序の誤解
: this(…) や : base(…) の本体は自分のコンストラクタ本体より先に実行されます。「委譲先でフィールドを設定してから自分のコンストラクタ本体で使える」と誤解することがありますが、実際には委譲先の処理が先に完了してから自分の本体が動きます。フィールドへの代入順序に注意してください。
よくある質問
async Task にできません。非同期初期化が必要な場合は、プライベートコンストラクタ + static async Task<T> CreateAsync() というファクトリメソッドパターンを使います。var obj = await MyClass.CreateAsync(); のように呼び出せます。ToString・分解が自動実装されます。C# 12 のクラス用プライマリコンストラクタはボイラープレートを減らすための糖衣構文で、パラメーターの自動プロパティ化・値等価は行われません。データ保持が目的なら record、サービスクラスの DI 注入簡略化が目的なら C# 12 プライマリコンストラクタという使い分けです。: base(…) を明示する必要があります。省略すると親の引数なしコンストラクタが呼ばれますが、それが存在しないとコンパイルエラーになります。継承を提供するクラスは引数なしコンストラクタを残すか、子クラスがどう呼ぶかのドキュメントを用意しましょう。まとめ
| コンストラクタの種類 | 特徴 | 主な用途 |
|---|---|---|
| 基本コンストラクタ | 引数ありでフィールドを初期化・バリデーション | 必須データの強制・整合性保証 |
| デフォルトコンストラクタ | 引数なし。手動定義しなければ自動生成 | 省略可能設定・シリアライザ対応 |
| オーバーロード + : this | 複数の引数パターン。委譲で DRY 化 | 省略パターンの提供 |
| : base(…) | 親クラスのコンストラクタを呼び出す | 継承クラスの初期化チェーン |
| 静的コンストラクタ | クラス初回アクセス時に1度だけ実行 | 静的フィールドの複雑な初期化 |
| プライベートコンストラクタ | 外部から直接 new を禁止 | ファクトリメソッド・シングルトン |
| プライマリコンストラクタ(C# 12) | クラス宣言にパラメーターを直接書く | DI 注入のボイラープレート削減 |
クラス設計の基礎全体はクラスとオブジェクト完全ガイドを、継承コンストラクタと base の詳細は継承とオーバーライドの基本を参照してください。