【C#】コンストラクタ完全ガイド|static・primaryコンストラクタ・継承・private・設計原則まで

コンストラクタは 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歳)
コンストラクタの3つのルール
① クラス名と同じ名前にする
② 戻り値型(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 に保ちます。

オーバーロードと : this 委譲
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
: this(…) を使うと検証・変換ロジックが1か所にまとまります。追加のオーバーロードは「何を省略したいか」だけ書けばよいため、後からバリデーションを変更しても全コンストラクタに反映されます。

フィールド初期化子とコンストラクタの実行順序

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(…) で親クラスのコンストラクタを呼び出せます。省略すると親の引数なしコンストラクタが自動的に呼ばれますが、親に引数なしコンストラクタがない場合はコンパイルエラーになります。

継承と : 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 からプライマリコンストラクタがクラスと構造体に導入されました。クラス宣言の直後にパラメーターリストを書くことで、コンストラクタ本体を省略できます。

プライマリコンストラクタ(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
DIコンテナとの組み合わせ(最も実用的なユースケース)
// 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);
    }
}
プライマリコンストラクタのパラメーター(xy)はクラス全体のスコープで参照できますが、フィールドとして自動生成されるわけではありません。プロパティへの割り当て(= 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か所にまとめ、他のオーバーロードは委譲のみ

よくある落とし穴と注意点

コンストラクタ内で仮想メソッドを呼ぶ

NG: コンストラクタ内で virtual メソッド呼び出し
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 だが未初期化!
コンストラクタ内で virtualabstract メソッドや protected なオーバーライド対象メソッドを呼ぶのは設計上のバグになりやすいです。代わりにファクトリメソッドで生成後に初期化処理を呼ぶパターンを使いましょう。

静的コンストラクタで例外が発生する

静的コンストラクタ内で例外がスローされると TypeInitializationException でラップされ、以降そのクラスは一切使用できなくなります(アプリ再起動まで)。外部リソース(環境変数・ファイルなど)にアクセスする場合は、存在しない場合のデフォルト値を用意するか、Lazy<T> を使って遅延初期化にしましょう。

引数ありコンストラクタを定義してデフォルト生成が使えなくなる

引数ありコンストラクタを1つでも定義した瞬間、コンパイラが自動生成するデフォルトコンストラクタが消えます。シリアライザ(System.Text.Json・EF Core など)は引数なしコンストラクタを要求することがあるため、気づかずに実行時エラーになるケースがあります。シリアライズが必要なクラスには public MyClass() { } を明示的に追加するか、コンストラクタに [JsonConstructor] 属性を付けてください。

: this / : base の呼び出し順序の誤解

: this(…): base(…) の本体は自分のコンストラクタ本体よりに実行されます。「委譲先でフィールドを設定してから自分のコンストラクタ本体で使える」と誤解することがありますが、実際には委譲先の処理が先に完了してから自分の本体が動きます。フィールドへの代入順序に注意してください。

よくある質問

Qコンストラクタとファクトリメソッドはどう使い分けますか?
Aシンプルな生成(引数がそのまま対応するフィールドに入る)はコンストラクタが適しています。「Celsius から作る」「Fahrenheit から作る」のように複数の生成方法に意味のある名前をつけたい場合、バリデーションが複雑な場合、非同期処理が必要な場合は static ファクトリメソッドを使いましょう。
Qコンストラクタに async/await は使えますか?
Aコンストラクタは戻り値を持てないため async Task にできません。非同期初期化が必要な場合は、プライベートコンストラクタ + static async Task<T> CreateAsync() というファクトリメソッドパターンを使います。var obj = await MyClass.CreateAsync(); のように呼び出せます。
Qプライマリコンストラクタ(C# 12)は既存の record と何が違いますか?
Arecord はプライマリコンストラクタのパラメーターが自動的に読み取り専用プロパティになり、値等価・ToString・分解が自動実装されます。C# 12 のクラス用プライマリコンストラクタはボイラープレートを減らすための糖衣構文で、パラメーターの自動プロパティ化・値等価は行われません。データ保持が目的なら record、サービスクラスの DI 注入簡略化が目的なら C# 12 プライマリコンストラクタという使い分けです。
Q親クラスにコンストラクタを定義すると子クラスに影響しますか?
Aはい、影響します。親クラスに引数ありコンストラクタしかない場合、子クラスのコンストラクタで : base(…) を明示する必要があります。省略すると親の引数なしコンストラクタが呼ばれますが、それが存在しないとコンパイルエラーになります。継承を提供するクラスは引数なしコンストラクタを残すか、子クラスがどう呼ぶかのドキュメントを用意しましょう。
QDI(依存性注入)ではどのコンストラクタが使われますか?
AASP.NET Core の標準 DI コンテナはデフォルトで「引数の多いコンストラクタ」を選択します(Microsoft.Extensions.DependencyInjection の仕様)。複数のコンストラクタがある場合、すべての引数をDIコンテナが解決できるものが選ばれます。解決できないコンストラクタが複数あると例外になるため、サービスクラスはコンストラクタを1つにするのが原則です。詳しくは依存性注入(DI)の基本と実装例を参照してください。

まとめ

コンストラクタの種類 特徴 主な用途
基本コンストラクタ 引数ありでフィールドを初期化・バリデーション 必須データの強制・整合性保証
デフォルトコンストラクタ 引数なし。手動定義しなければ自動生成 省略可能設定・シリアライザ対応
オーバーロード + : this 複数の引数パターン。委譲で DRY 化 省略パターンの提供
: base(…) 親クラスのコンストラクタを呼び出す 継承クラスの初期化チェーン
静的コンストラクタ クラス初回アクセス時に1度だけ実行 静的フィールドの複雑な初期化
プライベートコンストラクタ 外部から直接 new を禁止 ファクトリメソッド・シングルトン
プライマリコンストラクタ(C# 12) クラス宣言にパラメーターを直接書く DI 注入のボイラープレート削減

クラス設計の基礎全体はクラスとオブジェクト完全ガイドを、継承コンストラクタと base の詳細は継承とオーバーライドの基本を参照してください。