C#は強力なオブジェクト指向言語であり、その中心的な機能の一つに「継承」があります。
クラスを継承することで既存のコードを再利用し、拡張性の高いプログラムを設計することが可能になりますが、そこで多くの開発者が最初に直面する壁がコンストラクタの挙動です。
継承関係にあるクラスにおいて、インスタンスが生成される際に「どの順番でコンストラクタが呼ばれるのか」「基底クラスに引数をどう渡すのか」を正しく理解することは、バグのない堅牢なアプリケーションを開発する上で不可欠です。
本記事では、C#における継承クラスのコンストラクタの基礎から、実戦的な引数の受け渡し、さらには最新のC# 12で導入された機能まで、プロフェッショナルの視点で詳しく解説していきます。
C#における継承とコンストラクタの基本原則
まず大前提として理解しておくべき重要なルールは、コンストラクタは継承されないという点です。
メソッドやプロパティは派生クラスに引き継がれますが、コンストラクタは各クラス固有の初期化処理を担当するため、派生クラスで独自に定義する必要があります。
基底クラスの初期化は必須
派生クラスのインスタンスを生成する際、その内部には基底クラスのインスタンスとしての要素も含まれています。
そのため、派生クラスのコンストラクタが実行される前に、必ず基底クラスのコンストラクタが呼び出され、基底部分の初期化が完了している必要があります。
もし派生クラスのコンストラクタで明示的に基底クラスのコンストラクタを指定しなかった場合、コンパイラは自動的に基底クラスの「引数なしのコンストラクタ (デフォルトコンストラクタ)」を呼び出そうとします。
baseキーワードの役割
基底クラスの特定のコンストラクタを明示的に呼び出したい場合には、baseキーワードを使用します。
これは派生クラスのコンストラクタ宣言の直後に記述し、基底クラスに必要な引数を渡す役割を担います。
継承におけるコンストラクタの実行順序
オブジェクト指向プログラミングにおいて、初期化の順番は非常に重要です。
C#では、親から子へという順番でコンストラクタが実行されます。
このセクションでは、具体的なコードを用いてその挙動を確認してみましょう。
実行順序を確認するサンプルプログラム
以下のコードは、基底クラス(Parent)と派生クラス(Child)を定義し、それぞれのコンストラクタが呼ばれるタイミングをコンソールに出力するものです。
using System;
namespace ConstructorInheritanceDemo
{
// 基底クラス
public class Parent
{
public Parent()
{
Console.WriteLine("1. 基底クラス(Parent)のコンストラクタが実行されました。");
}
}
// 派生クラス
public class Child : Parent
{
public Child()
{
Console.WriteLine("2. 派生クラス(Child)のコンストラクタが実行されました。");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("--- インスタンス生成開始 ---");
Child child = new Child();
Console.WriteLine("--- インスタンス生成終了 ---");
}
}
}
--- インスタンス生成開始 ---
1. 基底クラス(Parent)のコンストラクタが実行されました。
2. 派生クラス(Child)のコンストラクタが実行されました。
--- インスタンス生成終了 ---
なぜ親が先なのか
この実行順序には明確な論理的理由があります。
派生クラスは基底クラスの機能に依存しているため、土台となる親の初期化が完了していない状態で、子(派生クラス)がその機能を利用することは危険だからです。
例えば、親クラスでリストを初期化し、子クラスでそのリストに要素を追加する場合、親の初期化が先に行われないとNull参照例外 (NullReferenceException) が発生してしまいます。
引数を持つコンストラクタの継承とbaseキーワード
基底クラスに引数が必要なコンストラクタしか定義されていない場合、派生クラスでは必ずその引数を渡すように記述しなければなりません。
ここで使用するのが、先述した : base() 構文です。
基本的な引数の渡し方
派生クラスのコンストラクタが受け取った値を、そのまま基底クラスにパススルーする実装が一般的です。
using System;
namespace ConstructorArgsDemo
{
// 基底クラス:名前を必須とする
public class Animal
{
public string Name { get; }
public Animal(string name)
{
this.Name = name;
Console.WriteLine($"Animalクラスの初期化: 名前 = {this.Name}");
}
}
// 派生クラス:Animalを継承
public class Dog : Animal
{
public string Breed { get; }
// base(name) を使って基底クラスに引数を渡す
public Dog(string name, string breed) : base(name)
{
this.Breed = breed;
Console.WriteLine($"Dogクラスの初期化: 種類 = {this.Breed}");
}
}
class Program
{
static void Main(string[] args)
{
Dog myDog = new Dog("ポチ", "柴犬");
Console.WriteLine($"結果: {myDog.Name} は {myDog.Breed} です。");
}
}
}
Animalクラスの初期化: 名前 = ポチ
Dogクラスの初期化: 種類 = 柴犬
結果: ポチ は 柴犬 です。
引数の加工と固定値の渡し方
base() に渡す値は、派生クラスの引数そのものである必要はありません。
計算結果や固定の文字列を渡すことも可能です。
public class Robot : Animal
{
// 固定の名前を基底クラスに渡す例
public Robot(int modelNumber) : base($"Robot-Model-{modelNumber}")
{
Console.WriteLine("ロボットが起動しました。");
}
}
このように、派生クラスの設計に応じて柔軟に基底クラスへデータを供給できます。
ただし、base() の引数にインスタンスメソッドの結果を使用することはできないという制約に注意してください。
基底クラスのコンストラクタが呼ばれる時点では、自身のインスタンスがまだ完全には構築されていないためです。
コンストラクタの連鎖 (Constructor Chaining)
同一クラス内の別のコンストラクタを呼び出す this() と、基底クラスのコンストラクタを呼び出す base() を組み合わせることで、複雑な初期化ロジックを整理できます。
これを「コンストラクタの連鎖」と呼びます。
複数のコンストラクタを持つ場合の挙動
以下の表は、各キーワードがどのコンストラクタを指すかをまとめたものです。
| キーワード | 呼び出し先 | 主な用途 |
|---|---|---|
base(...) | 親クラスのコンストラクタ | 親クラスのプロパティやフィールドの初期化を委譲する |
this(...) | 自クラスの別コンストラクタ | デフォルト値の設定など、初期化ロジックの重複を避ける |
これらを組み合わせる際の注意点は、一つのコンストラクタにつき base または this のどちらか一方しか指定できないというルールです。
両方を同時に記述することはできませんが、this() で呼び出した先のコンストラクタが base() を呼び出している場合は、最終的に基底クラスまで初期化が伝播します。
抽象クラス (abstract) におけるコンストラクタ
抽象クラスは直接インスタンス化することはできませんが、コンストラクタを持つことができます。
このコンストラクタの役割は、派生クラスで共通して必要となる初期化処理を一箇所に集約することです。
抽象クラスでの実装例
public abstract class Shape
{
public string Color { get; }
// 抽象クラスのコンストラクタ
protected Shape(string color)
{
this.Color = color;
}
public abstract double GetArea();
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(string color, double radius) : base(color)
{
this.Radius = radius;
}
public override double GetArea() => Math.PI * Radius * Radius;
}
抽象クラスのコンストラクタは、外部から new されることがないため、アクセス修飾子を protected にするのが一般的です。
これにより、設計の意図がより明確になります。
C# 12の最新機能:プライマリコンストラクタ
モダンなC#開発において避けては通れないのが、C# 12から導入されたプライマリコンストラクタです。
これにより、クラス定義とコンストラクタを一体化させ、コードを劇的に簡潔に記述できるようになりました。
継承とプライマリコンストラクタの書き方
プライマリコンストラクタを使用する場合でも、継承のルールは変わりません。
派生クラスの宣言部で、基底クラスに引数を渡す記述を行います。
using System;
// 基底クラスのプライマリコンストラクタ
public class Employee(string name, int id)
{
public string Name => name;
public int Id => id;
}
// 派生クラスのプライマリコンストラクタ
// クラス名の直後に引数を書き、継承部分で base の代わりに引数を渡す
public class Manager(string name, int id, string department) : Employee(name, id)
{
public string Department => department;
public void DisplayInfo()
{
Console.WriteLine($"ID: {Id}, 氏名: {Name}, 部署: {Department}");
}
}
class Program
{
static void Main()
{
var manager = new Manager("田中 太郎", 1001, "開発部");
manager.DisplayInfo();
}
}
ID: 1001, 氏名: 田中 太郎, 部署: 開発部
プライマリコンストラクタを使用すると、冗長なフィールドへの代入処理を記述する必要がなくなり、ボイラープレートコード(定型文)の大幅な削減が可能になります。
特に依存性注入 (Dependency Injection) を多用する現代のASP.NET Core開発などでは、非常に有用な機能です。
注意すべきアンチパターンとトラブルシューティング
コンストラクタの継承を扱う上で、避けるべきいくつかの落とし穴があります。
1. 基底クラスに引数なしコンストラクタがない場合のエラー
基底クラスで明示的に引数付きコンストラクタを定義すると、デフォルトの引数なしコンストラクタは生成されなくなります。
この状態で派生クラスが base() を省略すると、コンパイルエラー(CS7036)が発生します。
エラー解決策:
- 基底クラスに引数なしのコンストラクタを追加する。
- 派生クラスですべてのコンストラクタに
base(引数)を明示的に記述する。
2. コンストラクタ内での仮想メソッドの呼び出し
これは最も危険なプラクティスの一つです。
基底クラスのコンストラクタ内で、virtual 指定されたメソッドを呼び出すと、派生クラス側でオーバーライドされたメソッドが実行されます。
しかし、この時点では派生クラスのコンストラクタはまだ実行されていないため、派生クラスのフィールドが未初期化状態(nullなど)であり、予期せぬ実行時エラーを引き起こす可能性があります。
3. 深すぎる継承階層
継承が何層にも重なると、コンストラクタの引数の受け渡しが非常に複雑になります。
引数が多すぎる場合は、オブジェクトをまとめる「パラメータオブジェクト」パターンの導入や、継承よりも「コンポジション(合成)」を検討すべきタイミングかもしれません。
まとめ
C#における継承クラスのコンストラクタは、オブジェクトのライフサイクルを制御する非常に重要な要素です。
本記事で解説した主要なポイントを改めて整理します。
- 継承の順序: コンストラクタは常に「基底クラス(親)」から「派生クラス(子)」の順に実行される。
- baseキーワード: 基底クラスのコンストラクタに引数を渡す際は、派生クラスのコンストラクタ宣言に
: base(...)を付与する。 - 明示的な呼び出し: 基底クラスにデフォルトコンストラクタがない場合、派生クラスでの明示的な
base呼び出しは必須となる。 - モダンな記述: C# 12以降では、プライマリコンストラクタを利用することで、より簡潔に継承関係を記述できる。
コンストラクタの仕組みを正しく理解し、適切に使い分けることで、コードの可読性とメンテナンス性は飛躍的に向上します。
特に大規模なシステム開発では、初期化の不備が深刻なバグに直結するため、本記事で紹介した原則を常に意識して設計に取り組んでください。






