C#の継承とは?基底クラス・派生クラスの書き方からoverrideまで初心者向けに解説

はじめに

C#の継承は、既存のクラスが持つプロパティやメソッドを、新しいクラスへ引き継ぐための仕組みです。共通する処理を基底クラスへまとめることで、重複したコードを減らし、変更しやすいプログラムを作れます。

一方で、継承は単なるコード再利用の手段ではありません。クラス同士に適切な関係がないまま継承を使うと、クラス間の依存が強くなり、かえって保守しにくくなることがあります。

この記事では、C#の継承について、基底クラスと派生クラスの基本的な書き方から、basevirtualoverrideabstractsealed、ポリモーフィズムまで初心者向けに解説します。

1. C#の継承とは

1-1. 継承は既存クラスの機能を引き継ぐ仕組み

継承とは、既存のクラスをもとにして、新しいクラスを定義する仕組みです。

たとえば、すべての動物に名前と鳴く動作がある場合、共通部分をAnimalクラスに定義できます。そのうえで、犬を表すDogクラスや猫を表すCatクラスにAnimalクラスの機能を引き継がせます。

C#
public class Animal
{
public string Name { get; set; } = "";

public void Eat()
{
Console.WriteLine($"{Name}が食事をしています。");
}
}

public class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name}がワンワンと鳴きました。");
}
}

DogクラスにはNameプロパティやEatメソッドを記述していません。しかし、Animalクラスを継承しているため、これらのメンバーを利用できます。

1-2. 基底クラス(親クラス)と派生クラス(子クラス)の関係

継承元となるクラスを、C#では基底クラスと呼びます。継承によって作られるクラスは派生クラスです。

一般的には、次のような呼び方も使われます。

  • 基底クラス:親クラス、スーパークラス

  • 派生クラス:子クラス、サブクラス

先ほどの例では、Animalが基底クラス、Dogが派生クラスです。

C#
public class Dog : Animal
{
}

コロンの右側に記述されたAnimalが基底クラス、左側のDogが派生クラスになります。

派生クラスは基底クラスの特徴を引き継ぎながら、独自のプロパティやメソッドを追加できます。

1-3. 継承を使うメリット

C#で継承を使う主なメリットは、次のとおりです。

1つ目は、共通処理をまとめられることです。複数のクラスで同じプロパティやメソッドを使用する場合、基底クラスへ定義すれば重複を減らせます。

2つ目は、変更箇所を集約できることです。共通処理の仕様が変わっても、基底クラスを修正するだけで対応できる場合があります。

3つ目は、派生クラスを基底クラス型として統一的に扱えることです。これはポリモーフィズムと呼ばれる重要な特徴につながります。

4つ目は、派生クラスに共通のルールを持たせられることです。抽象クラスを使えば、必要なメソッドの実装を派生クラスへ強制できます。

1-4. 継承が適している「is-a」の関係

継承が適しているかを判断するときは、「派生クラスは基底クラスの一種である」と自然に説明できるかを考えます。これをis-a関係と呼びます。

たとえば、次の関係は自然です。

  • 犬は動物の一種である

  • 猫は動物の一種である

  • 正社員は従業員の一種である

  • 円は図形の一種である

一方、次のような関係に継承を使うのは適切ではありません。

  • 車はエンジンの一種である

  • パソコンはキーボードの一種である

車はエンジンを「持っている」のであり、エンジンの一種ではありません。このような関係はhas-a関係と呼ばれ、継承ではなく、クラスのフィールドやプロパティとして別のオブジェクトを持たせるコンポジションが適しています。

2. C#でクラスを継承する基本的な書き方

2-1. 基底クラスを定義する

最初に、共通する機能を持つ基底クラスを定義します。

C#
public class Vehicle
{
public string Name { get; set; } = "";

public void Start()
{
Console.WriteLine($"{Name}が動き始めました。");
}
}

このVehicleクラスには、乗り物の名前を表すNameプロパティと、動き始める処理を表すStartメソッドがあります。

基底クラスを作るときは、派生クラスに共通する状態や処理を定義します。

2-2. コロン(:)を使って派生クラスを定義する

C#でクラスを継承するには、クラス名の後ろにコロンを記述し、その右側に基底クラス名を指定します。

C#
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}

書式は次のとおりです。

C#
アクセス修飾子 class 派生クラス名 : 基底クラス名
{
}

CarクラスはVehicleクラスを継承しているため、NameプロパティとStartメソッドを利用できます。また、NumberOfDoorsのような独自のメンバーも追加できます。

2-3. 派生クラスから継承したメソッドを呼び出す

継承したメソッドは、派生クラスのインスタンスから通常のメソッドと同じように呼び出せます。

C#
Car car = new Car();

car.Name = "ファミリーカー";
car.Start();

実行結果は次のようになります。

ファミリーカーが動き始めました。

StartメソッドはVehicleクラスに定義されていますが、Carクラスのインスタンスから直接呼び出せます。

派生クラス内から呼び出すことも可能です。

C#
public class Car : Vehicle
{
public void Drive()
{
Start();
Console.WriteLine($"{Name}が走行しています。");
}
}

2-4. 継承したプロパティやフィールドを利用する

アクセス可能なプロパティやフィールドも派生クラスで利用できます。

C#
public class Employee
{
public string Name { get; set; } = "";
protected decimal Salary;
}

public class Manager : Employee
{
public void SetSalary(decimal salary)
{
Salary = salary;
}

public void ShowInformation()
{
Console.WriteLine($"名前:{Name}");
Console.WriteLine($"給与:{Salary}円");
}
}

Namepublicなので、クラスの内外から利用できます。Salaryprotectedなので、外部からは直接アクセスできませんが、派生クラスのManager内では利用できます。

ただし、フィールドを直接公開すると値の変更を管理しにくくなります。通常は、必要に応じてプロパティを利用するほうが安全です。

2-5. 多段階の継承を記述する

C#では、ある派生クラスをさらに別のクラスが継承できます。

C#
public class LivingThing
{
public void Breathe()
{
Console.WriteLine("呼吸します。");
}
}

public class Animal : LivingThing
{
public void Eat()
{
Console.WriteLine("食事をします。");
}
}

public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("ワンワン");
}
}

DogクラスはAnimalを継承し、AnimalLivingThingを継承しています。そのため、Dogのインスタンスから3つのメソッドを利用できます。

C#
Dog dog = new Dog();

dog.Breathe();
dog.Eat();
dog.Bark();

このような仕組みを多段階継承と呼びます。ただし、継承階層が深くなると、メソッドの定義場所や処理の流れが分かりにくくなります。必要以上に階層を増やさないことが大切です。

3. 継承されるメンバーとアクセス修飾子

3-1. publicメンバーは派生クラスから利用できる

publicを付けたメンバーは、派生クラスだけでなく、クラスの外部からも利用できます。

C#
public class Person
{
public string Name { get; set; } = "";

public void Introduce()
{
Console.WriteLine($"私の名前は{Name}です。");
}
}

public class Student : Person
{
public void Study()
{
Console.WriteLine($"{Name}が勉強しています。");
Introduce();
}
}

Studentクラスは、基底クラスのNameIntroduceを利用できます。

ただし、すべてのメンバーをpublicにすると、外部から自由に操作されてしまいます。公開する必要があるメンバーだけに付けましょう。

3-2. protectedメンバーは派生クラス内で利用できる

protectedメンバーは、定義したクラス自身と、その派生クラス内からアクセスできます。

C#
public class Account
{
protected decimal Balance { get; set; }

public void Deposit(decimal amount)
{
Balance += amount;
}
}

public class SavingsAccount : Account
{
public void ShowBalance()
{
Console.WriteLine($"残高は{Balance}円です。");
}
}

Balanceprotectedなので、SavingsAccount内では利用できます。しかし、次のように外部から直接アクセスすることはできません。

C#
SavingsAccount account = new SavingsAccount();

// コンパイルエラー
// account.Balance = 10000;

派生クラスへ提供したいものの、一般の利用者には公開したくないメンバーに適しています。

3-3. privateメンバーには派生クラスから直接アクセスできない

privateメンバーへアクセスできるのは、原則として、そのメンバーを定義したクラス内だけです。

C#
public class User
{
private string password = "";

public void SetPassword(string value)
{
password = value;
}
}

public class Administrator : User
{
public void ResetPassword()
{
// コンパイルエラー
// password = "new-password";

SetPassword("new-password");
}
}

passwordフィールドはAdministratorから直接参照できません。ただし、基底クラスが公開しているSetPasswordメソッドを通じて変更できます。

privateメンバーが基底クラスの内部状態として存在しなくなるわけではありません。派生クラスのオブジェクトにも基底クラス部分は含まれますが、派生クラスのコードから直接アクセスできないという意味です。

3-4. internalとprotected internalの違い

internalメンバーは、同じアセンブリ内からアクセスできます。アセンブリは、一般的にはコンパイルによって作られるDLLや実行ファイルの単位です。

C#
public class Service
{
internal void ExecuteInternalProcess()
{
Console.WriteLine("内部処理を実行します。");
}
}

同じプロジェクトやアセンブリ内であれば利用できますが、別のアセンブリからは通常アクセスできません。

protected internalは、次のどちらかを満たす場所からアクセスできます。

  • 同じアセンブリ内である

  • 派生クラス内である

C#
public class BaseService
{
protected internal void Execute()
{
Console.WriteLine("処理を実行します。");
}
}

なお、C#にはprivate protectedもあります。これは、同じアセンブリ内にある派生クラスからだけアクセスできる指定です。

3-5. アクセス修飾子を使い分けるポイント

アクセス修飾子は、できるだけ公開範囲が狭いものを選ぶのが基本です。

クラス外部へ公開する必要がある場合はpublic、派生クラスだけに公開したい場合はprotectedを検討します。クラス内部だけで使うものはprivateにします。

ただし、派生クラスで使う可能性があるという理由だけで、多くのメンバーをprotectedにするのは避けましょう。protectedメンバーを増やすと、派生クラスが基底クラスの内部実装へ強く依存します。

内部状態を操作させたい場合は、フィールドをprotectedで公開するより、検証処理を含むメソッドやプロパティを提供するほうが安全です。

4. 基底クラスのメンバーを参照するbaseキーワード

4-1. baseを使って基底クラスのメソッドを呼び出す

baseキーワードを使うと、派生クラスから基底クラスのメンバーを明示的に参照できます。

C#
public class Message
{
public virtual void Show()
{
Console.WriteLine("基本メッセージ");
}
}

public class ErrorMessage : Message
{
public override void Show()
{
base.Show();
Console.WriteLine("エラーが発生しました。");
}
}

ErrorMessageShowでは、最初にbase.Show()で基底クラスの処理を呼び出し、その後に独自処理を追加しています。

実行すると、次のように表示されます。

基本メッセージ
エラーが発生しました。

4-2. baseを使って基底クラスのプロパティへアクセスする

基底クラスのプロパティが派生クラスでオーバーライドされている場合も、baseを使って元のプロパティへアクセスできます。

C#
public class Product
{
public virtual decimal Price { get; set; }
}

public class TaxIncludedProduct : Product
{
public override decimal Price
{
get => base.Price * 1.1m;
set => base.Price = value;
}
}

この例では、基底クラス側に税抜価格を保存し、派生クラス側で税込価格を返しています。

C#
TaxIncludedProduct product = new TaxIncludedProduct
{
Price = 1000m
};

Console.WriteLine(product.Price);

出力結果は1100.0です。

4-3. 派生クラスから基底クラスのコンストラクターを呼び出す

派生クラスのコンストラクターから基底クラスのコンストラクターを呼び出す場合は、コンストラクターの後ろに: base(...)を記述します。

C#
public class Person
{
public string Name { get; }

public Person(string name)
{
Name = name;
}
}

public class Employee : Person
{
public int EmployeeNumber { get; }

public Employee(string name, int employeeNumber)
: base(name)
{
EmployeeNumber = employeeNumber;
}
}

Employeeのコンストラクターは、受け取ったnamebase(name)Personのコンストラクターへ渡しています。

C#
Employee employee = new Employee("田中", 1001);

Console.WriteLine(employee.Name);
Console.WriteLine(employee.EmployeeNumber);

4-4. コンストラクターが実行される順番

派生クラスのインスタンスを作成すると、基底クラスのコンストラクターが先に実行され、その後で派生クラスのコンストラクターが実行されます。

C#
public class Parent
{
public Parent()
{
Console.WriteLine("Parentのコンストラクター");
}
}

public class Child : Parent
{
public Child()
{
Console.WriteLine("Childのコンストラクター");
}
}
C#
Child child = new Child();

実行結果は次のとおりです。

Parentのコンストラクター
Childのコンストラクター

多段階継承の場合も、最上位の基底クラスから順番に実行されます。これにより、派生クラスの初期化が始まる前に、基底クラス部分の初期化が完了します。

4-5. 基底クラスに引数なしコンストラクターがない場合の対処法

派生クラスのコンストラクターでbaseを省略すると、コンパイラは基底クラスの引数なしコンストラクターを呼び出そうとします。

ところが、基底クラスに引数付きコンストラクターしかない場合は、コンパイルエラーになります。

C#
public class Item
{
public string Name { get; }

public Item(string name)
{
Name = name;
}
}

public class Book : Item
{
// base()で呼び出せるコンストラクターがないためエラー
// public Book()
// {
// }
}

この場合は、派生クラスのコンストラクターから必要な引数を渡します。

C#
public class Book : Item
{
public Book(string name)
: base(name)
{
}
}

または、設計上必要であれば、基底クラスに引数なしコンストラクターを追加します。ただし、無効な初期状態が作られるような引数なしコンストラクターを、エラー回避だけのために追加するのは避けましょう。

5. virtualとoverrideによるメソッドのオーバーライド

5-1. オーバーライドとは

オーバーライドとは、基底クラスで定義されたメソッドの処理を、派生クラスで置き換えることです。

たとえば、すべての動物にSpeakメソッドを持たせつつ、犬と猫で異なる鳴き声を出したい場合に利用できます。

C#
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("動物が鳴きます。");
}
}

派生クラスは、このメソッドをオーバーライドして独自の動作を定義できます。

5-2. 基底クラスのメソッドにvirtualを付ける

派生クラスでオーバーライドできるようにするには、基底クラスのメソッドへvirtualを付けます。

C#
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("動物が鳴きます。");
}
}

virtualは、そのメソッドに既定の実装があり、必要に応じて派生クラスが変更できることを表します。

virtualを付けていない通常のメソッドは、overrideできません。

5-3. 派生クラスのメソッドにoverrideを付ける

派生クラス側では、オーバーライドするメソッドへoverrideを付けます。

C#
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}

public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー");
}
}

メソッド名だけでなく、戻り値や引数なども、オーバーライド可能な基底クラスのメソッドと対応している必要があります。

C#
Dog dog = new Dog();
dog.Speak();

実行結果は次のとおりです。

ワンワン

5-4. overrideしたメソッドからbaseの処理を呼び出す

基底クラスの処理を完全に置き換えるのではなく、処理を追加したい場合はbaseを使います。

C#
public class Logger
{
public virtual void Write(string message)
{
Console.WriteLine(message);
}
}

public class TimestampLogger : Logger
{
public override void Write(string message)
{
string formattedMessage =
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";

base.Write(formattedMessage);
}
}

派生クラスではメッセージへ日時を追加し、実際の出力処理は基底クラスへ任せています。

基底クラスの処理を呼び出すべきかどうかは、メソッドの設計によって異なります。必ず呼び出す必要がある処理なら、派生クラスが呼び忘れない構造を検討することも重要です。

5-5. プロパティをオーバーライドする方法

メソッドだけでなく、プロパティにもvirtualoverrideを使用できます。

C#
public class User
{
public virtual string DisplayName { get; set; } = "ユーザー";
}

public class PremiumUser : User
{
public override string DisplayName
{
get => $"★ {base.DisplayName}";
set => base.DisplayName = value;
}
}
C#
PremiumUser user = new PremiumUser
{
DisplayName = "佐藤"
};

Console.WriteLine(user.DisplayName);

実行結果は次のようになります。

★ 佐藤

基底クラスと派生クラスでアクセサーの構成やアクセス範囲に関する制約があるため、オーバーライド時はコンパイラエラーを確認しながら定義しましょう。

5-6. overrideとオーバーロードの違い

オーバーライドとオーバーロードは名前が似ていますが、異なる機能です。

オーバーライドは、継承関係にあるクラスで、基底クラスのメソッドを派生クラスが置き換える仕組みです。

C#
public class BaseClass
{
public virtual void Execute()
{
}
}

public class DerivedClass : BaseClass
{
public override void Execute()
{
}
}

オーバーロードは、同じ名前で引数が異なるメソッドを複数定義する仕組みです。継承は必須ではありません。

C#
public class Calculator
{
public int Add(int left, int right)
{
return left + right;
}

public double Add(double left, double right)
{
return left + right;
}

public int Add(int first, int second, int third)
{
return first + second + third;
}
}

オーバーライドは実装の置き換え、オーバーロードは引数の違いによる同名メソッドの使い分けです。

6. メソッドの隠蔽とnewキーワード

6-1. 同名メソッドを定義したときに発生するメソッドの隠蔽

基底クラスにある通常のメソッドと同じ名前・同じ引数のメソッドを派生クラスに定義すると、基底クラスのメソッドが隠されます。

C#
public class Parent
{
public void Show()
{
Console.WriteLine("Parent.Show");
}
}

public class Child : Parent
{
public void Show()
{
Console.WriteLine("Child.Show");
}
}

このコードでは、意図しないメソッド隠蔽である可能性を知らせる警告が表示されます。

Child型の変数から呼び出せばChild.Showが実行されますが、Parent型として扱った場合はParent.Showが実行されます。

6-2. newキーワードを使ったメソッドの再定義

意図的に基底クラスのメソッドを隠す場合は、派生クラスのメソッドへnewを付けます。

C#
public class Parent
{
public void Show()
{
Console.WriteLine("Parent.Show");
}
}

public class Child : Parent
{
public new void Show()
{
Console.WriteLine("Child.Show");
}
}

newを付けることで警告を抑えられ、意図的な隠蔽であることをコード上で明確にできます。

ただし、newは基底クラスのメソッドをオーバーライドするものではありません。変数の宣言型によって、呼び出されるメソッドが変わります。

6-3. overrideとnewの動作の違い

overrideでは、実際のオブジェクトの型に応じて実行されるメソッドが決まります。

C#
public class BaseOverride
{
public virtual void Show()
{
Console.WriteLine("BaseOverride.Show");
}
}

public class DerivedOverride : BaseOverride
{
public override void Show()
{
Console.WriteLine("DerivedOverride.Show");
}
}

一方、newで隠蔽したメソッドは、基本的に変数の宣言型に応じて選ばれます。

C#
public class BaseNew
{
public void Show()
{
Console.WriteLine("BaseNew.Show");
}
}

public class DerivedNew : BaseNew
{
public new void Show()
{
Console.WriteLine("DerivedNew.Show");
}
}

ポリモーフィズムを実現したい場合は、通常、virtualoverrideを使います。newは、基底クラスと派生クラスで意図的に別のメンバーとして扱う場合に使用します。

6-4. 基底クラス型の変数から呼び出した場合の違い

overridenewの違いは、基底クラス型の変数へ代入すると明確になります。

まず、overrideの例です。

C#
BaseOverride value1 = new DerivedOverride();
value1.Show();

実行結果は次のとおりです。

DerivedOverride.Show

変数の型はBaseOverrideですが、実際のオブジェクトがDerivedOverrideなので、オーバーライド後のメソッドが呼ばれます。

次に、newの例です。

C#
BaseNew value2 = new DerivedNew();
value2.Show();

実行結果は次のとおりです。

BaseNew.Show

newによる隠蔽では、BaseNew型の変数から呼び出しているため、基底クラス側のメソッドが選ばれます。

6-5. 意図しないメソッド隠蔽を避ける方法

意図しないメソッド隠蔽を避けるには、コンパイラの警告を無視しないことが重要です。

派生クラスで基底クラスの動作を置き換えたい場合は、基底クラスへvirtual、派生クラスへoverrideを付けます。

基底クラスのメソッドを変更できず、意図的に別の動作を定義する場合はnewを使います。ただし、呼び出し元の変数型によって動作が変わるため、利用者が混乱しやすい設計です。

同名にする明確な理由がなければ、異なる役割を表すメソッド名を付ける方法も検討しましょう。

7. abstractを使った抽象クラスの継承

7-1. 抽象クラスとは

抽象クラスとは、直接インスタンス化せず、派生クラスの共通基盤として使うクラスです。クラス宣言へabstractを付けて定義します。

C#
public abstract class Shape
{
}

次のコードはコンパイルエラーになります。

C#
// 抽象クラスは直接インスタンス化できない
// Shape shape = new Shape();

抽象クラスには、通常のフィールド、プロパティ、コンストラクター、実装済みメソッドを定義できます。さらに、処理内容を派生クラスに任せる抽象メソッドも定義できます。

7-2. abstractメソッドを定義する

抽象メソッドは、メソッド本体を持たず、派生クラスに実装を要求するメソッドです。

C#
public abstract class Shape
{
public abstract double GetArea();
}

抽象メソッドには処理ブロックを記述せず、宣言の末尾にセミコロンを付けます。

abstractメソッドを定義できるのは抽象クラス内だけです。そのため、抽象メソッドを1つでも持つクラスにはabstractを付ける必要があります。

7-3. 派生クラスで抽象メソッドを実装する

抽象クラスを継承した具象クラスは、原則として抽象メソッドをすべてオーバーライドする必要があります。

C#
public class Circle : Shape
{
public double Radius { get; }

public Circle(double radius)
{
Radius = radius;
}

public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
C#
Circle circle = new Circle(5);
Console.WriteLine(circle.GetArea());

派生クラス自身もabstractとして定義する場合は、未実装の抽象メソッドを残すことができます。

C#
public abstract class SpecialShape : Shape
{
}

7-4. virtualメソッドとabstractメソッドの違い

virtualメソッドには基底クラス側の実装があります。派生クラスは必要な場合だけオーバーライドします。

C#
public virtual void Draw()
{
Console.WriteLine("図形を描画します。");
}

abstractメソッドには基底クラス側の実装がなく、具象クラスは実装しなければなりません。

C#
public abstract void Draw();

標準的な処理を用意できる場合はvirtual、派生クラスごとに必ず異なる処理が必要な場合はabstractが適しています。

7-5. 抽象クラスを使うべきケース

抽象クラスは、複数の派生クラスに共通の状態や処理を提供しつつ、一部の処理を派生クラスへ任せたい場合に適しています。

たとえば、決済処理の共通手順は決まっているものの、実際の支払い処理だけが決済方法ごとに異なるケースです。

C#
public abstract class Payment
{
public void Execute(decimal amount)
{
Validate(amount);
ProcessPayment(amount);
Console.WriteLine("決済が完了しました。");
}

protected virtual void Validate(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(amount),
"金額は0より大きい必要があります。");
}
}

protected abstract void ProcessPayment(decimal amount);
}

この設計では、処理全体の流れを基底クラスへまとめ、決済方法固有の処理だけを派生クラスへ実装させられます。

8. 継承を制限するsealedキーワード

8-1. sealedクラスで継承を禁止する

クラスへsealedを付けると、そのクラスを継承できなくなります。

C#
public sealed class ApplicationSettings
{
public string Language { get; set; } = "ja";
}

次のコードはコンパイルエラーになります。

C#
// sealedクラスは継承できない
// public class CustomSettings : ApplicationSettings
// {
// }

派生クラスによって振る舞いを変更されると困るクラスや、継承を前提として設計していないクラスに使用します。

なお、staticクラスは暗黙的に継承できないクラスとして扱われます。

8-2. sealed overrideで再オーバーライドを禁止する

クラス全体ではなく、オーバーライドしたメソッドだけを、それ以降の派生クラスで変更できないようにすることも可能です。

C#
public class BaseProcessor
{
public virtual void Process()
{
Console.WriteLine("基本処理");
}
}

public class FixedProcessor : BaseProcessor
{
public sealed override void Process()
{
Console.WriteLine("固定された処理");
}
}

public class CustomProcessor : FixedProcessor
{
// Processはsealed overrideなので再オーバーライドできない
// public override void Process()
// {
// }
}

FixedProcessor自体は継承できますが、Processメソッドの動作は固定されます。

8-3. 継承を禁止したほうがよいケース

次のような場合は、sealedの使用を検討できます。

  • クラスの不変条件を派生クラスに壊されたくない

  • セキュリティ上、処理の差し替えを認めたくない

  • 継承を前提とした拡張ポイントを設計していない

  • 値として完結するクラスで、派生型を必要としない

  • 将来も派生クラスをサポートする予定がない

公開クラスを継承可能にすると、利用者が派生クラスを作る可能性があります。その後、基底クラスの内部実装を変更すると、既存の派生クラスに影響するかもしれません。

継承を正式な拡張手段として提供しない場合は、最初からsealedにするという設計もあります。

9. 継承とポリモーフィズム

9-1. 派生クラスのインスタンスを基底クラス型で扱う

派生クラスのインスタンスは、基底クラス型の変数へ代入できます。これをアップキャストと呼びます。

C#
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("動物が鳴きます。");
}
}

public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}
C#
Animal animal = new Dog();

変数の型はAnimalですが、実際に作られているオブジェクトはDogです。

この仕組みにより、異なる派生クラスを基底クラス型として統一的に扱えます。

9-2. オーバーライドしたメソッドが実行される仕組み

基底クラス型の変数からvirtualメソッドを呼び出すと、実際のオブジェクト型に対応するオーバーライド済みメソッドが実行されます。

C#
Animal animal = new Dog();
animal.Speak();

実行結果は次のとおりです。

ワンワン

このように、同じAnimal型の変数から同じSpeakメソッドを呼び出しても、実際のオブジェクトによって動作が変わる性質をポリモーフィズムと呼びます。

呼び出し側は、犬や猫などの具体的な種類を詳しく知らなくても、共通の方法で処理できます。

9-3. 基底クラス型から派生クラス型へキャストする

基底クラス型の変数を派生クラス型へ変換することをダウンキャストと呼びます。

C#
Animal animal = new Dog();
Dog dog = (Dog)animal;

実際のオブジェクトがDogであれば変換できます。

しかし、実際のオブジェクトが異なる型の場合、InvalidCastExceptionが発生します。

C#
public class Cat : Animal
{
}

Animal animal = new Cat();

// 実行時にInvalidCastException
// Dog dog = (Dog)animal;

確実に型が分かっている場合を除き、直接キャストする前に型を確認しましょう。

9-4. is演算子とパターンマッチングで型を判定する

is演算子を使うと、オブジェクトが特定の型として扱えるかを確認できます。

C#
Animal animal = new Dog();

if (animal is Dog)
{
Console.WriteLine("Dog型です。");
}

型の確認と変換を同時に行うパターンマッチングが便利です。

C#
if (animal is Dog dog)
{
dog.Speak();
}

animalDogとして扱える場合だけ、変換後の変数dogが作られます。

派生型ごとに処理を分ける場合は、switch式やswitch文のパターンマッチングも利用できます。

C#
string result = animal switch
{
Dog => "犬です。",
Cat => "猫です。",
_ => "その他の動物です。"
};

Console.WriteLine(result);

9-5. as演算子を使って安全に変換する

as演算子は、参照型を指定した型へ変換します。変換できなかった場合は例外ではなくnullを返します。

C#
Animal animal = new Dog();

Dog? dog = animal as Dog;

if (dog is not null)
{
dog.Speak();
}

変換に失敗する可能性があり、失敗時にnullとして扱いたい場合に利用できます。

ただし、型の判定後すぐに変数を使うだけなら、次のパターンマッチングのほうが簡潔です。

C#
if (animal is Dog matchedDog)
{
matchedDog.Speak();
}

10. C#の継承に関するルールと制約

10-1. クラスは複数のクラスを継承できない

C#のクラスが直接継承できるクラスは1つだけです。

次のような複数継承はできません。

C#
public class ClassA
{
}

public class ClassB
{
}

// コンパイルエラー
// public class ClassC : ClassA, ClassB
// {
// }

複数の役割を持たせたい場合は、インターフェースやコンポジションを使います。

C#
public interface IPrintable
{
void Print();
}

public interface ISavable
{
void Save();
}

public class Document : IPrintable, ISavable
{
public void Print()
{
Console.WriteLine("印刷します。");
}

public void Save()
{
Console.WriteLine("保存します。");
}
}

10-2. 構造体はクラスや構造体を継承できない

structで定義する構造体は、任意のクラスや別の構造体を継承できません。

C#
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}

構造体は値型であり、クラスとは異なる継承ルールを持ちます。ただし、インターフェースを実装することは可能です。

C#
public interface IResettable
{
void Reset();
}

public struct Counter : IResettable
{
public int Value { get; private set; }

public void Reset()
{
Value = 0;
}
}

10-3. コンストラクターは継承されない

基底クラスのコンストラクターは派生クラスへ継承されません。

C#
public class Person
{
public Person(string name)
{
}
}

public class Employee : Person
{
public Employee(string name)
: base(name)
{
}
}

Person(string name)が存在しても、Employee(string name)が自動的に作られるわけではありません。派生クラスで必要なコンストラクターを定義し、baseを使って基底クラスのコンストラクターを呼び出します。

10-4. staticクラスとsealedクラスは継承できない

staticクラスはインスタンス化できず、継承元にも派生クラスにもできません。

C#
public static class MathHelper
{
public static int Square(int value)
{
return value * value;
}
}

sealedクラスはインスタンス化できますが、継承元にはできません。

C#
public sealed class FinalClass
{
}

どちらも継承できない点は同じですが、目的が異なります。staticはインスタンスを持たない機能の集合、sealedは継承を禁止した通常のクラスです。

10-5. すべてのクラスが継承するObjectクラス

C#のクラスは、明示的な基底クラスを指定していなくても、最終的にはSystem.Objectを継承します。C#では通常、objectというキーワードで表します。

C#
public class Sample
{
}

このクラスは、概念的には次のような関係です。

C#
public class Sample : object
{
}

そのため、すべてのクラスでToStringEqualsGetHashCodeGetTypeなどのメソッドを利用できます。

C#
Sample sample = new Sample();

Console.WriteLine(sample.ToString());
Console.WriteLine(sample.GetType().Name);

独自クラスでToStringの表示内容を変えたい場合は、オーバーライドできます。

C#
public class Product
{
public string Name { get; set; } = "";

public override string ToString()
{
return $"商品名:{Name}";
}
}

11. 継承とインターフェースの違い

11-1. クラス継承は共通の状態と処理を引き継ぐ

クラス継承では、基底クラスが持つフィールド、プロパティ、実装済みメソッドなどを派生クラスへ引き継げます。

C#
public class Employee
{
public string Name { get; set; } = "";

public void ClockIn()
{
Console.WriteLine($"{Name}が出勤しました。");
}
}

public class Engineer : Employee
{
}

Engineerは、従業員としての状態と処理をそのまま利用できます。

ただし、クラスは1つの基底クラスしか直接継承できません。また、基底クラスの内部設計へ依存しやすい点にも注意が必要です。

11-2. インターフェースは実装すべき契約を定義する

インターフェースは、その型が提供すべき機能の契約を定義します。

C#
public interface IWorker
{
void Work();
}

実装するクラスは、契約に対応するメンバーを用意します。

C#
public class Engineer : IWorker
{
public void Work()
{
Console.WriteLine("プログラムを開発します。");
}
}

インターフェースは、異なる継承階層に属するクラスへ共通の役割を持たせたい場合に便利です。

11-3. インターフェースは複数実装できる

C#のクラスは1つのクラスしか直接継承できませんが、複数のインターフェースを実装できます。

C#
public interface IReadable
{
void Read();
}

public interface IWritable
{
void Write();
}

public class FileDocument : IReadable, IWritable
{
public void Read()
{
Console.WriteLine("ファイルを読み込みます。");
}

public void Write()
{
Console.WriteLine("ファイルへ書き込みます。");
}
}

基底クラスの継承とインターフェースの実装を同時に行う場合は、基底クラスを最初に記述します。

C#
public class Document
{
}

public class FileDocument : Document, IReadable, IWritable
{
public void Read()
{
}

public void Write()
{
}
}

11-4. 抽象クラスとインターフェースの使い分け

抽象クラスは、密接に関連する派生クラスへ共通の状態や実装を提供したい場合に適しています。

インターフェースは、継承階層に関係なく共通の役割や能力を表したい場合に適しています。

たとえば、Animalのように共通の名前や年齢を持ち、処理の一部も共通化したい場合は抽象クラスが候補になります。

一方、IFlyableのように「飛べる」という能力を表す場合はインターフェースが適しています。鳥だけでなく、飛行機やドローンにも実装できるためです。

判断に迷った場合は、次の観点で考えます。

  • 共通の状態を持たせたいか

  • 共通実装を強く共有したいか

  • 複数の役割を組み合わせたいか

  • 対象同士に自然なis-a関係があるか

11-5. 継承とインターフェースを組み合わせる例

クラス継承とインターフェースは、どちらか一方だけを選ぶ必要はありません。

C#
public abstract class Animal
{
public string Name { get; }

protected Animal(string name)
{
Name = name;
}

public abstract void Speak();
}

public interface ITrainable
{
void PerformTrick();
}

public class Dog : Animal, ITrainable
{
public Dog(string name)
: base(name)
{
}

public override void Speak()
{
Console.WriteLine($"{Name}:ワンワン");
}

public void PerformTrick()
{
Console.WriteLine($"{Name}がお手をしました。");
}
}

DogAnimalの一種であり、同時に訓練可能という役割を持っています。このように、種類を表す継承と、能力を表すインターフェースを組み合わせられます。

12. 継承を使った実践的なサンプルコード

12-1. AnimalクラスをDogクラスとCatクラスが継承する例

ここでは、動物を表すAnimalクラスを作り、DogCatが継承する例を考えます。

C#
public class Animal
{
public string Name { get; set; } = "";

public void Eat()
{
Console.WriteLine($"{Name}が食事をしています。");
}
}

public class Dog : Animal
{
public void Fetch()
{
Console.WriteLine($"{Name}がボールを取ってきました。");
}
}

public class Cat : Animal
{
public void Climb()
{
Console.WriteLine($"{Name}が木に登りました。");
}
}

DogCatは、共通のNameEatを継承し、それぞれ独自のメソッドを持っています。

12-2. virtualとoverrideで鳴き声を変更する

動物ごとに鳴き声を変更するため、基底クラスへvirtualメソッドを追加します。

C#
public class Animal
{
public string Name { get; set; } = "";

public virtual void Speak()
{
Console.WriteLine($"{Name}が鳴きました。");
}
}

public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine($"{Name}:ワンワン");
}
}

public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine($"{Name}:ニャー");
}
}
C#
Dog dog = new Dog { Name = "ポチ" };
Cat cat = new Cat { Name = "ミケ" };

dog.Speak();
cat.Speak();

実行結果は次のとおりです。

ポチ:ワンワン
ミケ:ニャー

12-3. baseコンストラクターで共通データを初期化する

名前を必須にしたい場合は、基底クラスのコンストラクターで初期化します。

C#
public class Animal
{
public string Name { get; }
public int Age { get; }

public Animal(string name, int age)
{
Name = name;
Age = age;
}

public virtual void Speak()
{
Console.WriteLine($"{Name}が鳴きました。");
}
}

public class Dog : Animal
{
public Dog(string name, int age)
: base(name, age)
{
}

public override void Speak()
{
Console.WriteLine($"{Name}:ワンワン");
}
}

public class Cat : Animal
{
public Cat(string name, int age)
: base(name, age)
{
}

public override void Speak()
{
Console.WriteLine($"{Name}:ニャー");
}
}

DogCatのコンストラクターは、受け取った値をbaseAnimalへ渡しています。

C#
Dog dog = new Dog("ポチ", 3);

Console.WriteLine($"{dog.Name}は{dog.Age}歳です。");

12-4. 基底クラス型のコレクションで派生クラスを一括処理する

継承とポリモーフィズムを使うと、異なる派生クラスを同じコレクションへ格納できます。

C#
List<Animal> animals = new List<Animal>
{
new Dog("ポチ", 3),
new Cat("ミケ", 5),
new Dog("コタロウ", 2)
};

foreach (Animal animal in animals)
{
animal.Speak();
}

実行結果は次のようになります。

ポチ:ワンワン
ミケ:ニャー
コタロウ:ワンワン

ループ内ではDogCatを個別に判定していません。各オブジェクトに対応したSpeakが自動的に実行されます。

これがC#の継承を利用する大きな利点の1つです。

12-5. 抽象クラスを使って派生クラスに実装を強制する

すべての動物が必ず固有の鳴き方を実装する設計にするなら、Animalを抽象クラスにします。

C#
public abstract class Animal
{
public string Name { get; }
public int Age { get; }

protected Animal(string name, int age)
{
Name = name;
Age = age;
}

public void ShowProfile()
{
Console.WriteLine($"名前:{Name}、年齢:{Age}歳");
}

public abstract void Speak();
}

public class Dog : Animal
{
public Dog(string name, int age)
: base(name, age)
{
}

public override void Speak()
{
Console.WriteLine($"{Name}:ワンワン");
}
}

public class Cat : Animal
{
public Cat(string name, int age)
: base(name, age)
{
}

public override void Speak()
{
Console.WriteLine($"{Name}:ニャー");
}
}

完全な実行例は次のとおりです。

C#
List<Animal> animals = new()
{
new Dog("ポチ", 3),
new Cat("ミケ", 5)
};

foreach (Animal animal in animals)
{
animal.ShowProfile();
animal.Speak();
}

共通するプロフィール表示は基底クラスへまとめ、種類ごとに異なる鳴き声は派生クラスへ実装させています。

13. C#の継承で発生しやすいエラーと対処法

13-1. overrideできる適切なメソッドがないと表示される

派生クラスでoverrideを付けたものの、対応するオーバーライド可能なメソッドが基底クラスにない場合、コンパイルエラーになります。

C#
public class BaseClass
{
public void Execute()
{
}
}

public class DerivedClass : BaseClass
{
// BaseClass.Executeにvirtualがないためエラー
// public override void Execute()
// {
// }
}

基底クラスのメソッドを変更できる場合は、virtualを付けます。

C#
public class BaseClass
{
public virtual void Execute()
{
}
}

また、メソッド名、引数、戻り値などが対応しているかも確認します。意図的な隠蔽であればnewを使えますが、overrideとは動作が異なります。

13-2. アクセスできない保護レベルと表示される

基底クラスのprivateメンバーへ派生クラスからアクセスすると、保護レベルに関するコンパイルエラーが発生します。

C#
public class BaseClass
{
private int value;
}

public class DerivedClass : BaseClass
{
public void SetValue()
{
// privateなのでアクセスできない
// value = 10;
}
}

派生クラスから直接利用する必要がある場合は、protectedプロパティやprotectedメソッドを提供します。

C#
public class BaseClass
{
private int value;

protected void SetValue(int newValue)
{
value = newValue;
}
}

ただし、安易にprivateprotectedへ変更するのではなく、どの操作を派生クラスへ許可するかを検討しましょう。

13-3. 基底クラスのコンストラクターに必要な引数が不足する

基底クラスに引数なしコンストラクターがなく、派生クラスから必要な引数を渡していない場合、コンパイルエラーになります。

C#
public class Person
{
public Person(string name)
{
}
}

public class Employee : Person
{
// Person()が存在しないためエラー
// public Employee()
// {
// }
}

: base(...)を使って、必要な引数を渡します。

C#
public class Employee : Person
{
public Employee(string name)
: base(name)
{
}
}

エラーが出たら、基底クラスにどのコンストラクターが定義されているかを確認してください。

13-4. sealedクラスを継承してコンパイルエラーになる

sealedクラスは継承できません。

C#
public sealed class FinalService
{
}

// コンパイルエラー
// public class CustomService : FinalService
// {
// }

この場合は、継承ではなく、対象クラスのインスタンスをフィールドとして保持する方法を検討します。

C#
public class CustomService
{
private readonly FinalService service = new FinalService();
}

ただし、元のクラスが必要な拡張ポイントを提供していない場合、ラッパークラスを作ってもすべての処理を変更できるとは限りません。

13-5. キャストに失敗してInvalidCastExceptionが発生する

実際のオブジェクト型と異なる派生クラスへ直接キャストすると、InvalidCastExceptionが発生します。

C#
Animal animal = new Cat("ミケ", 5);

// 実際はCatなので失敗する
// Dog dog = (Dog)animal;

パターンマッチングで型を確認してから処理します。

C#
if (animal is Dog dog)
{
dog.Speak();
}
else
{
Console.WriteLine("Dog型ではありません。");
}

または、変換失敗をnullとして扱う場合はasを使います。

C#
Dog? dog = animal as Dog;

if (dog is not null)
{
dog.Speak();
}

キャストを頻繁に必要とする設計では、基底クラスやインターフェースに必要な処理を定義できないか見直すことも重要です。

14. 継承を使う際の注意点と設計のコツ

14-1. コードの再利用だけを目的に継承しない

同じ処理を使いたいという理由だけで継承すると、不自然なクラス関係になることがあります。

たとえば、プリンター機能を再利用するためにReport : Printerとするのは適切とは限りません。レポートはプリンターの一種ではないためです。

この場合は、ReportPrinterを利用する構成が自然です。

C#
public class Printer
{
public void Print(string content)
{
Console.WriteLine(content);
}
}

public class Report
{
private readonly Printer printer;

public Report(Printer printer)
{
this.printer = printer;
}

public void PrintReport()
{
printer.Print("レポート内容");
}
}

継承は、コードが共通しているかだけでなく、意味としてis-a関係が成立するかで判断します。

14-2. 継承階層を深くしすぎない

継承階層が深くなるほど、クラスの動作を理解するために多くの基底クラスを確認しなければなりません。

たとえば、次のような階層があるとします。

Object
└─ Entity
└─ Character
└─ Enemy
└─ FlyingEnemy
└─ BossFlyingEnemy

階層が深いと、プロパティやメソッドの定義場所、オーバーライドの有無、コンストラクターの流れが分かりにくくなります。

共通処理を小さなクラスへ分け、コンポジションとして組み合わせることで、階層を浅くできる場合があります。

14-3. 基底クラスの変更が派生クラスへ与える影響を考える

基底クラスの変更は、すべての派生クラスへ影響する可能性があります。

たとえば、基底クラスのvirtualメソッドに新しい処理を追加しても、派生クラスが完全にオーバーライドしていれば、その処理は実行されません。

また、基底クラスが新しいvirtualメソッドを追加したとき、派生クラスに同名メソッドがすでにあると、隠蔽や警告が発生することがあります。

基底クラスを変更するときは、次の点を確認します。

  • 既存の派生クラスがどのメソッドをオーバーライドしているか

  • コンストラクターの変更が初期化処理へ影響しないか

  • protectedメンバーの変更で派生クラスが壊れないか

  • メソッドの事前条件や戻り値の意味が変わらないか

14-4. 継承よりコンポジションが適しているケース

コンポジションとは、別のクラスのオブジェクトをフィールドやプロパティとして持ち、その機能を利用する設計です。

C#
public class Engine
{
public void Start()
{
Console.WriteLine("エンジンを始動します。");
}
}

public class Car
{
private readonly Engine engine;

public Car(Engine engine)
{
this.engine = engine;
}

public void Start()
{
engine.Start();
Console.WriteLine("車が走行可能になりました。");
}
}

CarEngineの一種ではなく、Engineを持っています。そのため、継承よりコンポジションが自然です。

次のような場合もコンポジションが候補になります。

  • 実行時に利用する機能を切り替えたい

  • 複数の機能を自由に組み合わせたい

  • 基底クラスの内部実装へ依存したくない

  • is-a関係よりhas-a関係が自然である

  • 単体テストで依存先を差し替えたい

14-5. 継承させるメンバーを必要最小限にする

派生クラスから利用できるメンバーが多いほど、派生クラスは基底クラスの内部構造へ依存しやすくなります。

特に、変更可能なフィールドをprotectedで公開すると、どの派生クラスがどのように値を変更しているか把握しにくくなります。

C#
public class Account
{
private decimal balance;

protected decimal Balance => balance;

protected void AddBalance(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}

balance += amount;
}
}

フィールドそのものを公開するのではなく、必要な情報を読み取るプロパティや、ルールを守って更新するメソッドを提供すると安全です。

基底クラスは、派生クラスへ何を許可するかを明確に設計しましょう。

15. C#の継承に関するよくある質問

15-1. C#では複数のクラスを継承できる?

C#のクラスが直接継承できるクラスは1つだけです。複数のクラスを同時に基底クラスとして指定する多重継承はできません。

ただし、複数のインターフェースは実装できます。

C#
public class Sample : BaseClass, IFirst, ISecond
{
}

共通実装を複数のクラスから再利用したい場合は、コンポジションも検討してください。

15-2. privateメンバーは派生クラスに継承される?

privateメンバーは派生クラスから直接アクセスできません。

派生クラスのインスタンスにも基底クラス部分は含まれるため、privateフィールドが内部状態として存在することはあります。しかし、派生クラスのコードからその名前を使って参照することはできません。

必要な操作は、基底クラスが提供するpublicまたはprotectedのプロパティやメソッドを通じて行います。

15-3. virtualを付けずにoverrideできる?

通常のメソッドを、派生クラスでそのままoverrideすることはできません。

基底クラス側のメソッドが、virtualabstract、またはすでにoverrideされたオーバーライド可能なメソッドである必要があります。

C#
public class BaseClass
{
public virtual void Execute()
{
}
}

public class DerivedClass : BaseClass
{
public override void Execute()
{
}
}

同名メソッドをnewで隠すことはできますが、オーバーライドとは異なり、変数の宣言型によって呼び出される処理が変わります。

15-4. baseとthisの違いは?

thisは、現在のインスタンスを表します。現在のクラスにあるフィールド、プロパティ、メソッド、コンストラクターを参照するときに使います。

C#
public class Person
{
private readonly string name;

public Person(string name)
{
this.name = name;
}
}

baseは、現在のインスタンスに含まれる基底クラス部分を表します。基底クラスのメソッドやコンストラクターを明示的に呼び出すときに使います。

C#
public class Employee : Person
{
public Employee(string name)
: base(name)
{
}
}

コンストラクター初期化子では、this(...)を使って同じクラスの別のコンストラクターを呼び出すこともできます。

C#
public class Product
{
public string Name { get; }
public decimal Price { get; }

public Product(string name)
: this(name, 0m)
{
}

public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}

15-5. 継承とインスタンス化の違いは?

継承は、既存クラスをもとに新しいクラスを定義する仕組みです。

C#
public class Dog : Animal
{
}

インスタンス化は、クラスをもとに実際のオブジェクトを生成することです。

C#
Dog dog = new Dog();

クラスは設計図、インスタンスは設計図から作られた実体と考えると分かりやすいでしょう。

なお、抽象クラスは継承元として利用できますが、直接インスタンス化することはできません。

15-6. UnityのMonoBehaviourも継承の一種?

Unityで次のように記述するコードも、C#のクラス継承です。

C#
using UnityEngine;

public class PlayerController : MonoBehaviour
{
private void Start()
{
Debug.Log("ゲーム開始");
}
}

PlayerControllerクラスが、UnityのMonoBehaviourクラスを継承しています。

これにより、Unityのコンポーネントとしてゲームオブジェクトへ追加でき、StartUpdateなど、Unityが呼び出すライフサイクルに対応したメソッドを定義できます。

ただし、StartUpdateは一般的な意味でvirtualメソッドをoverrideしているとは限りません。Unity側が特定の名前と形式のメソッドを検出して呼び出す仕組みです。そのため、通常はoverrideを付けずに記述します。

まとめ

C#の継承は、基底クラスの機能を派生クラスへ引き継ぎ、共通処理をまとめるための仕組みです。

派生クラスは、クラス名の後ろにコロンと基底クラス名を記述して定義します。

C#
public class Dog : Animal
{
}

基底クラスの動作を派生クラスごとに変更したい場合は、virtualoverrideを使います。基底クラスのメンバーやコンストラクターを明示的に呼び出す場合はbaseを利用します。

また、abstractを使えば派生クラスに実装を強制でき、sealedを使えば継承や再オーバーライドを禁止できます。派生クラスを基底クラス型として扱うことで、ポリモーフィズムによる柔軟な処理も実現できます。

ただし、継承はコードを再利用できるという理由だけで使うものではありません。「派生クラスは基底クラスの一種である」というis-a関係が自然に成立するかを確認することが重要です。

is-a関係が成立しない場合や、機能を柔軟に組み合わせたい場合は、インターフェースやコンポジションも検討しましょう。継承、インターフェース、コンポジションを適切に使い分けることで、変更しやすく理解しやすいC#プログラムを設計できます。