C#で多重継承はできる?代替手段とインターフェース活用法をわかりやすく解説

はじめに

C#でオブジェクト指向プログラミングを学んでいると、「1つのクラスに複数のクラスの機能を継承させたい」と考える場面があります。しかし、C#ではクラスを使った多重継承はできません。

たとえば、AnimalクラスとMachineクラスの両方を継承したRobotDogクラスを作る、といった設計はC#の文法上認められていません。

一方で、C#には複数のインターフェースの実装、抽象クラス、コンポジション、拡張メソッドなど、多重継承の代わりになる仕組みが用意されています。目的に合った方法を選べば、多重継承を使わなくても柔軟で保守しやすい設計を実現できます。

この記事では、C#で多重継承ができない理由と、その代替手段についてサンプルコードを交えながらわかりやすく解説します。

1. C#で多重継承はできる?結論から解説

1-1. C#のクラスは多重継承できない

結論として、C#のクラスは複数のクラスを同時に継承できません。

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

public class Animal{public void Eat(){Console.WriteLine("食べます");}}

public class Machine{public void Start(){Console.WriteLine("起動します");}}

// コンパイルエラーpublic class RobotDog : Animal, Machine{}

C#では、クラスが継承できる基底クラスは1つだけです。そのため、AnimalMachineの両方を基底クラスとして指定することはできません。

ただし、クラスを1つ継承しながら、複数のインターフェースを実装することは可能です。

public class Animal{public void Eat(){Console.WriteLine("食べます");}}

public interface IMovable{void Move();}

public interface IRechargeable{void Recharge();}

public class RobotDog : Animal, IMovable, IRechargeable{public void Move(){Console.WriteLine("移動します");}

public void Recharge(){Console.WriteLine("充電します");}

}

この仕組みを利用することで、多重継承に近い設計を実現できます。

1-2. C#で許可されている継承の基本ルール

C#におけるクラスとインターフェースの主なルールは次のとおりです。

  • クラスが継承できるクラスは1つだけ

  • クラスは複数のインターフェースを実装できる

  • インターフェースは複数のインターフェースを継承できる

  • 基底クラスとインターフェースを同時に指定できる

  • 基底クラスを指定する場合は、インターフェースより先に記述する

たとえば、次の宣言は有効です。

public class Employee : Person, IWorker, IReportable{}

EmployeeクラスはPersonクラスを継承し、同時にIWorkerIReportableを実装しています。

一方、次のように複数のクラスを指定することはできません。

public class Employee : Person, Account{}

PersonAccountがどちらもクラスである場合、この宣言はコンパイルエラーになります。

1-3. 多重継承したい場面でよくある課題

多重継承を使いたくなるのは、主に1つのクラスに複数の役割や機能を持たせたい場合です。

たとえば、ゲームのキャラクターに「移動する」「攻撃する」「会話する」といった複数の能力を持たせたいケースがあります。また、業務システムでは、ユーザーに「承認する」「通知を送る」「ログを記録する」といった役割を与えたいこともあります。

このようなケースで、機能ごとに作成したクラスをすべて継承しようとすると、多重継承が必要になります。しかし、C#では複数のクラスを継承できないため、別の設計方法を選ばなければなりません。

重要なのは、「複数のクラスを継承すること」自体を目的にしないことです。必要なのが型としての役割なのか、共通処理なのか、状態を含む機能なのかを整理すれば、適切な代替手段を選びやすくなります。

1-4. この記事でわかる代替手段

C#で多重継承の代わりに利用できる主な方法は次のとおりです。

  • 複数の役割を表現するインターフェース

  • 共通の状態や処理を持たせる抽象クラス

  • 複数のオブジェクトを組み合わせるコンポジション

  • 既存の型に処理を追加する拡張メソッド

  • インターフェースに共通実装を持たせるデフォルトインターフェースメソッド

どの方法にも向いている用途と注意点があります。単純にインターフェースへ置き換えるだけではなく、設計の目的に合わせて使い分けることが大切です。

2. 多重継承とは何か

2-1. 多重継承の基本的な意味

多重継承とは、1つのクラスが複数の基底クラスを継承する仕組みです。

仮に多重継承が可能な言語で、BirdクラスとMachineクラスを継承する場合、次のような関係を作れます。

Bird ─────┐├─ FlyingRobotMachine ──┘

FlyingRobotは、Birdが持つ飛行機能と、Machineが持つ起動機能の両方を引き継げます。

一見すると便利ですが、複数の基底クラスに同じ名前のメソッドが存在する場合など、どの実装を使用するのかがわかりにくくなる問題があります。

2-2. 単一継承との違い

単一継承では、1つのクラスが直接継承できる基底クラスは1つだけです。C#のクラス継承は単一継承を採用しています。

public class Animal{public void Eat(){Console.WriteLine("食べます");}}

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

DogクラスはAnimalクラスだけを直接継承しています。ただし、継承を階層化することは可能です。

public class LivingThing{}

public class Animal : LivingThing{}

public class Dog : Animal{}

この場合、DogAnimalを通してLivingThingのメンバーも引き継ぎます。複数のクラスを横並びに継承することはできませんが、1本の継承階層を作ることはできます。

2-3. C++など多重継承できる言語との違い

C++などの言語では、クラスが複数のクラスを継承できます。

class Camera{public:void TakePhoto() {}};

class Phone{public:void Call() {}};

class SmartPhone : public Camera, public Phone{};

この例では、SmartPhoneクラスがCameraPhoneの機能を継承しています。

C#では、同じ考え方をクラスの多重継承で実装できません。代わりに、役割をインターフェースとして定義したり、必要な機能を持つオブジェクトをフィールドやプロパティとして保持したりします。

public interface ICamera{void TakePhoto();}

public interface IPhone{void Call();}

public class SmartPhone : ICamera, IPhone{public void TakePhoto(){Console.WriteLine("写真を撮影します");}

public void Call(){Console.WriteLine("電話をかけます");}

}

C#では、継承関係を増やすよりも、役割と実装を分離して組み合わせる設計が重視されています。

2-4. 多重継承が便利に見えるケース

多重継承は、既存クラスの機能をそのまま再利用したい場面で便利に見えます。

たとえば、次のような機能を持つクラスが存在するとします。

  • Loggerクラス:ログを出力する

  • Validatorクラス:入力値を検証する

  • Repositoryクラス:データを保存する

新しいサービスクラスでこれらすべてを使いたい場合、それぞれを継承できれば簡単に見えるかもしれません。

しかし、「ログ出力できるサービスはLoggerの一種である」とは限りません。これは「is-a」の関係ではなく、「Loggerを利用する」という「has-a」の関係です。

このような場合は、継承よりもコンポジションを使うほうが自然です。

3. C#がクラスの多重継承を禁止している理由

3-1. ダイヤモンド問題とは

多重継承で代表的な問題が、ダイヤモンド問題です。

次のような継承関係を考えてみましょう。

        Animal/      
Walker Swimmer\ /Duck

WalkerSwimmerがどちらもAnimalを継承し、DuckWalkerSwimmerの両方を継承すると、継承関係がひし形になります。

ここでAnimalMoveメソッドがあり、WalkerSwimmerがそれぞれ異なる方法でオーバーライドしていた場合、DuckからMoveを呼び出したときに、どちらの実装を使うべきかという問題が発生します。

また、基底クラスの状態が複数経路から継承される場合、同じ状態を1つだけ持つのか、経路ごとに別々に持つのかという問題も起こります。

C#ではクラスを単一継承に制限することで、このような曖昧さを避けています。

3-2. メソッドやプロパティの衝突が起きる問題

複数の基底クラスに同じ名前のメソッドやプロパティがあると、呼び出す対象が不明確になります。

たとえば、PrinterクラスとScannerクラスの両方にStartメソッドが存在するとします。

Printer.Start()Scanner.Start()

これらを継承したクラスで単にStart()を呼び出した場合、どちらを実行するのか判断するためのルールが必要です。

戻り値や引数が異なる場合はオーバーロードとして扱える可能性がありますが、シグネチャが同じで実装だけが異なる場合は衝突します。開発者が毎回優先順位を指定する仕組みにすると、コードの理解や保守が難しくなります。

C#のインターフェースでも同名メンバーが発生することはありますが、明示的インターフェース実装を使って呼び分けられます。クラスの実装や状態を丸ごと継承する場合と比べて、影響範囲を整理しやすい点が特徴です。

3-3. 設計が複雑になり保守性が下がる問題

多重継承を多用すると、クラスがどこから機能を受け継いでいるのか追跡しにくくなります。

あるメソッドの動作を確認するだけでも、複数の基底クラス、その基底クラス、オーバーライドの優先順位などを調べなければならない可能性があります。

また、基底クラスの変更が複数の継承経路を通じて派生クラスへ影響します。小さな変更が予想外の不具合につながりやすくなり、テストの範囲も広がります。

単一継承とインターフェース、コンポジションを組み合わせる設計では、継承階層と利用する機能を分離できます。依存関係が明確になるため、変更による影響を把握しやすくなります。

3-4. C#が安全でわかりやすい設計を優先している理由

C#では、プログラムの安全性や読みやすさ、保守性を重視した仕組みが採用されています。

クラスの継承を1つに限定すれば、基底クラスが明確になります。複数の役割が必要な場合はインターフェースを使い、複数の実装を利用したい場合はコンポジションを使うことで、目的ごとに設計を分けられます。

つまり、C#は多重継承に相当する機能を完全に排除しているのではありません。曖昧になりやすいクラスの多重継承を禁止し、より明示的な方法で機能を組み合わせられるようにしています。

4. C#で多重継承の代わりに使う主な方法

4-1. インターフェースを複数実装する

複数の役割や能力を1つのクラスに持たせたい場合は、インターフェースの複数実装が基本的な選択肢です。

public interface IWalkable{void Walk();}

public interface ISwimmable{void Swim();}

public class Duck : IWalkable, ISwimmable{public void Walk(){Console.WriteLine("歩きます");}

public void Swim(){Console.WriteLine("泳ぎます");}

}

Duckクラスは、IWalkableISwimmableという2つの役割を持っています。

インターフェースを型として利用すれば、具体的なクラスに依存せず処理できます。

public static void MakeWalk(IWalkable target){target.Walk();}

IWalkableを実装するクラスであれば、Duck以外でもMakeWalkへ渡せます。

4-2. 抽象クラスを活用する

複数の派生クラスに共通する状態や処理を持たせたい場合は、抽象クラスが適しています。

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

protected Animal(string name){Name = name;}public void Eat(){Console.WriteLine($"{Name}が食べます");}public abstract void Speak();

}

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

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

}

抽象クラスには、フィールド、プロパティ、コンストラクター、実装済みメソッド、抽象メソッドなどを定義できます。

ただし、クラスは抽象クラスも1つしか継承できません。複数の役割を追加したい場合は、抽象クラスとインターフェースを組み合わせます。

4-3. コンポジションで機能を組み合わせる

コンポジションとは、必要な機能を持つオブジェクトをクラスの内部に保持し、それらを組み合わせる設計です。

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

public class NavigationSystem{public void Navigate(){Console.WriteLine("目的地まで案内します");}}

public class Car{private readonly Engine _engine;private readonly NavigationSystem _navigationSystem;

public Car(Engine engine, NavigationSystem navigationSystem){_engine = engine;_navigationSystem = navigationSystem;}public void Start(){_engine.Start();}public void Navigate(){_navigationSystem.Navigate();}

}

CarEngineNavigationSystemを継承していません。それぞれを内部に持ち、必要な処理を委譲しています。

機能を交換しやすく、クラス同士の関係もわかりやすいため、実務では多重継承の有力な代替手段になります。

4-4. 拡張メソッドで処理を追加する

拡張メソッドを使うと、既存クラスを変更したり継承したりせずに、メソッドを追加したような形式で呼び出せます。

public static class StringExtensions{public static bool IsNotEmpty(this string value){return !string.IsNullOrEmpty(value);}}

次のように利用できます。

string name = "C#";

bool result = name.IsNotEmpty();Console.WriteLine(result);

拡張メソッドは、既存型に補助的な処理を追加する場合に便利です。ただし、実際にクラスのインスタンスメンバーが追加されるわけではありません。

また、フィールドやインスタンス状態を追加できず、通常の仮想メソッドのように派生クラスでオーバーライドすることもできません。そのため、継承の完全な代替ではなく、共通処理を提供するための補助的な手段と考えるべきです。

4-5. デフォルトインターフェースメソッドを使う

C# 8.0以降では、インターフェースのメソッドにデフォルト実装を記述できます。

public interface ILogger{void Log(string message){Console.WriteLine($"Log: {message}");}}

実装クラス側でメソッドを定義していなくても、インターフェース型を通してデフォルト実装を利用できます。

public class ApplicationService : ILogger{}

ILogger logger = new ApplicationService();logger.Log("処理を開始しました");

デフォルトインターフェースメソッドは、既存の実装クラスを壊さずにインターフェースへ機能を追加したい場合などに役立ちます。

ただし、インターフェースはインスタンスフィールドを持つための仕組みではなく、抽象クラスと同じではありません。複数のインターフェースから同じメソッドのデフォルト実装を継承すると、衝突を解決するために実装クラス側で処理を定義する必要があります。

5. インターフェースで多重継承のように実装する方法

5-1. インターフェースとは何か

インターフェースは、クラスや構造体が提供すべき機能を定める契約です。

たとえば、IPrintableインターフェースにPrintメソッドを定義した場合、実装するクラスは印刷機能を提供することを表します。

public interface IPrintable{void Print();}

実装クラスは次のように記述します。

public class Report : IPrintable{public void Print(){Console.WriteLine("レポートを印刷します");}}

利用側はReportという具体的なクラスではなく、IPrintableという役割に依存できます。

public static void ExecutePrint(IPrintable printable){printable.Print();}

この設計により、実装の差し替えやテストがしやすくなります。

5-2. 複数のインターフェースを実装する基本構文

複数のインターフェースを実装する場合は、クラス名の後ろにカンマ区切りで指定します。

public class ClassName : IInterfaceA, IInterfaceB, IInterfaceC{}

基底クラスも使用する場合は、最初に基底クラスを記述します。

public class ClassName : BaseClass, IInterfaceA, IInterfaceB{}

たとえば、Vehicleクラスを継承しながら、充電機能と自動運転機能を持つクラスは次のように定義できます。

public class Vehicle{public void Stop(){Console.WriteLine("停止します");}}

public interface IChargeable{void Charge();}

public interface IAutoDrivable{void AutoDrive();}

public class ElectricCar : Vehicle, IChargeable, IAutoDrivable{public void Charge(){Console.WriteLine("バッテリーを充電します");}

public void AutoDrive(){Console.WriteLine("自動運転を開始します");}

}

5-3. インターフェースを使ったサンプルコード

ファイルを保存し、ログも出力できるサービスを例にします。

public interface ISavable{void Save(string content);}

public interface ILoggable{void Log(string message);}

public class DocumentService : ISavable, ILoggable{public void Save(string content){Console.WriteLine($"保存しました: {content}");}

public void Log(string message){Console.WriteLine($"ログ: {message}");}

}

利用例は次のとおりです。

var service = new DocumentService();

service.Save("C#の学習メモ");service.Log("保存処理が完了しました");

インターフェース型として個別に扱うこともできます。

ISavable savable = service;ILoggable loggable = service;

savable.Save("設計資料");loggable.Log("資料を保存しました");

DocumentServiceは1つのクラスですが、保存可能なオブジェクトとログ出力可能なオブジェクトという複数の役割を持っています。

5-4. 同じ名前のメソッドがある場合の対処法

複数のインターフェースに同じ名前とシグネチャのメソッドがある場合、1つのパブリックメソッドで両方を実装できます。

public interface IPrinter{void Execute();}

public interface IScanner{void Execute();}

public class MultiFunctionDevice : IPrinter, IScanner{public void Execute(){Console.WriteLine("処理を実行します");}}

しかし、インターフェースごとに異なる処理を実行したい場合は、明示的インターフェース実装を使います。

public interface IPrinter{void Execute();}

public interface IScanner{void Execute();}

public class MultiFunctionDevice : IPrinter, IScanner{void IPrinter.Execute(){Console.WriteLine("印刷します");}

void IScanner.Execute(){Console.WriteLine("スキャンします");}

}

呼び出すときは、対象のインターフェース型へ変換します。

var device = new MultiFunctionDevice();

IPrinter printer = device;IScanner scanner = device;

printer.Execute();scanner.Execute();

明示的に実装したメソッドは、通常のクラス型から直接呼び出せません。

// device.Execute(); は呼び出せない

この仕組みによって、同名メソッドの衝突を明確に解決できます。

5-5. インターフェースを使うメリットと注意点

インターフェースを使う主なメリットは、複数の役割を柔軟に組み合わせられることです。利用側が具体的なクラスではなくインターフェースへ依存すれば、実装を差し替えやすくなります。

また、モックやスタブを用意しやすいため、単体テストにも向いています。

一方、インターフェースを細かく分けすぎると、型の数が増えて設計がわかりにくくなることがあります。反対に、1つのインターフェースへ無関係なメンバーを詰め込みすぎると、実装クラスに不要なメソッドまで実装させることになります。

インターフェースは、「その型が利用者に対してどのような役割を提供するか」を基準に設計することが重要です。

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

6-1. 抽象クラスを使うべきケース

抽象クラスは、関連性の高いクラス間で状態や処理を共有したい場合に適しています。

具体的には、次のようなケースです。

  • 派生クラスに共通するフィールドやプロパティを持たせたい

  • 共通のコンストラクター処理が必要

  • 一部の処理を共通化し、一部だけ派生クラスに実装させたい

  • protectedメンバーを使って派生クラスへ内部機能を公開したい

  • 明確な「is-a」の関係がある

たとえば、従業員を表すクラスに共通の名前と基本給を持たせる場合は、抽象クラスが適しています。

public abstract class Employee{public string Name { get; }protected decimal BaseSalary { get; }

protected Employee(string name, decimal baseSalary){Name = name;BaseSalary = baseSalary;}public abstract decimal CalculateSalary();public void ShowProfile(){Console.WriteLine($"従業員名: {Name}");}

}

派生クラスでは、給与計算だけを個別に実装できます。

public class FullTimeEmployee : Employee{public FullTimeEmployee(string name, decimal baseSalary): base(name, baseSalary){}

public override decimal CalculateSalary(){return BaseSalary;}

}

6-2. インターフェースを使うべきケース

インターフェースは、継承関係とは別に、複数の型へ共通の役割を持たせたい場合に適しています。

たとえば、「保存できる」という能力は、文書、画像、設定情報など、互いに異なるクラスに持たせられます。

public interface ISavable{void Save();}

public class Document : ISavable{public void Save(){Console.WriteLine("文書を保存します");}}

public class ApplicationSettings : ISavable{public void Save(){Console.WriteLine("設定を保存します");}}

DocumentApplicationSettingsに共通の親クラスを作る必要はありません。ISavableという役割だけを共有できます。

複数の役割を自由に組み合わせたい場合や、依存性注入を使って実装を交換したい場合にもインターフェースが向いています。

6-3. 抽象クラスとインターフェースを組み合わせる方法

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

共通の状態と基本処理を抽象クラスに持たせ、追加の役割をインターフェースで表現できます。

public abstract class Character{public string Name { get; }public int Health { get; protected set; }

protected Character(string name, int health){Name = name;Health = health;}public void ShowStatus(){Console.WriteLine($"{Name}: HP {Health}");}

}

public interface IAttackable{void Attack();}

public interface IHealable{void Heal();}

public class Paladin : Character, IAttackable, IHealable{public Paladin(string name): base(name, 100){}

public void Attack(){Console.WriteLine($"{Name}が剣で攻撃します");}public void Heal(){Health += 20;Console.WriteLine($"{Name}が回復しました");}

}

PaladinCharacterとして共通の状態を持ちながら、攻撃と回復という複数の役割を実装しています。

6-4. 使い分けで迷ったときの判断基準

抽象クラスとインターフェースの使い分けに迷った場合は、次の点を確認します。

共通の状態やコンストラクターを持たせたい場合は、抽象クラスが候補です。異なる種類のクラスへ共通の役割だけを与えたい場合は、インターフェースが適しています。

また、「そのクラスは基底クラスの一種である」と自然に説明できるかも重要です。

たとえば、「犬は動物の一種である」と説明できるため、Dog : Animalは自然です。一方、「注文サービスはロガーの一種である」とは言いにくいため、OrderService : Loggerよりも、ロガーを内部に保持するコンポジションが適しています。

継承は強い結合を作ります。明確な継承関係がない場合は、まずインターフェースやコンポジションを検討するとよいでしょう。

7. コンポジションで多重継承を避ける設計

7-1. コンポジションとは何か

コンポジションは、クラスが必要な機能を持つ別のオブジェクトを内部に保持する設計方法です。

継承が「AはBの一種である」という関係を表すのに対し、コンポジションは「AはBを持っている」「AはBを利用する」という関係を表します。

たとえば、パソコンはCPUやストレージを継承しているわけではありません。CPUやストレージを部品として持っています。

public class Cpu{public void Process(){Console.WriteLine("データを処理します");}}

public class Storage{public void Save(){Console.WriteLine("データを保存します");}}

public class Computer{private readonly Cpu _cpu;private readonly Storage _storage;

public Computer(Cpu cpu, Storage storage){_cpu = cpu;_storage = storage;}public void Execute(){_cpu.Process();_storage.Save();}

}

このように複数の部品を組み合わせれば、多重継承を使わずに複数の機能を持つクラスを作れます。

7-2. 継承よりコンポジションが推奨される理由

コンポジションでは、利用する機能を必要に応じて交換できます。継承の場合、基底クラスはクラス定義の一部であり、実行時に簡単に変更できません。

一方、コンストラクターなどを通して依存オブジェクトを受け取る設計にすれば、異なる実装を渡せます。

public interface IMessageSender{void Send(string message);}

public class EmailSender : IMessageSender{public void Send(string message){Console.WriteLine($"メール送信: {message}");}}

public class SmsSender : IMessageSender{public void Send(string message){Console.WriteLine($"SMS送信: {message}");}}

通知サービスは、具体的な送信方法を継承せず、インターフェースを通して利用します。

public class NotificationService{private readonly IMessageSender _messageSender;

public NotificationService(IMessageSender messageSender){_messageSender = messageSender;}public void Notify(string message){_messageSender.Send(message);}

}

利用時に実装を切り替えられます。

var emailService = new NotificationService(new EmailSender());emailService.Notify("メールで通知します");

var smsService = new NotificationService(new SmsSender());smsService.Notify("SMSで通知します");

この設計は変更に強く、テスト用の実装も渡しやすいという利点があります。

7-3. コンポジションを使ったサンプルコード

注文処理に、決済、在庫更新、ログ出力という複数の機能を組み合わせる例を考えます。

public interface IPaymentProcessor{void Pay(decimal amount);}

public interface IInventoryService{void ReduceStock(string productId, int quantity);}

public interface IAppLogger{void Log(string message);}

public class PaymentProcessor : IPaymentProcessor{public void Pay(decimal amount){Console.WriteLine($"{amount:N0}円を決済しました");}}

public class InventoryService : IInventoryService{public void ReduceStock(string productId, int quantity){Console.WriteLine($"{productId}の在庫を{quantity}個減らしました");}}

public class ConsoleAppLogger : IAppLogger{public void Log(string message){Console.WriteLine($"LOG: {message}");}}

注文サービスは、これらの実装を内部で利用します。

public class OrderService{private readonly IPaymentProcessor _paymentProcessor;private readonly IInventoryService _inventoryService;private readonly IAppLogger _logger;

public OrderService(IPaymentProcessor paymentProcessor,IInventoryService inventoryService,IAppLogger logger){_paymentProcessor = paymentProcessor;_inventoryService = inventoryService;_logger = logger;}public void PlaceOrder(string productId,int quantity,decimal amount){_logger.Log("注文処理を開始します");_paymentProcessor.Pay(amount);_inventoryService.ReduceStock(productId, quantity);_logger.Log("注文処理が完了しました");}

}

利用例は次のとおりです。

var orderService = new OrderService(new PaymentProcessor(),new InventoryService(),new ConsoleAppLogger());

orderService.PlaceOrder("ITEM-001", 2, 5000);

OrderServiceが決済クラス、在庫クラス、ログクラスを多重継承する必要はありません。必要な機能をオブジェクトとして受け取ることで、責務を分離できます。

7-4. 再利用性と保守性を高める設計ポイント

コンポジションを活用するときは、それぞれのクラスに1つの明確な責務を持たせることが重要です。

決済、保存、通知、ログ出力などの処理を小さなクラスへ分離すれば、他の機能からも再利用できます。また、実装クラスへ直接依存するのではなく、インターフェースを介して利用すると交換しやすくなります。

依存するオブジェクトは、クラス内部で直接生成するよりも、コンストラクターから受け取る方法が一般的です。

// 依存関係が固定されるpublic class BadService{private readonly EmailSender _sender = new EmailSender();}

次のように外部から受け取れば、実装を変更できます。

public class BetterService{private readonly IMessageSender _sender;

public BetterService(IMessageSender sender){_sender = sender;}

}

この方法は依存性注入と呼ばれ、ASP.NET Coreをはじめとする.NET開発でも広く利用されています。

8. C#の多重継承に関するよくある疑問

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

C#では、1つのクラスが複数のクラスを直接継承することはできません。

次のコードは無効です。

public class Sample : BaseClassA, BaseClassB{}

継承できる基底クラスは1つだけです。複数の機能が必要な場合は、インターフェース、コンポジション、拡張メソッドなどを利用します。

なお、継承を何段階にもつなげることはできます。

public class A{}

public class B : A{}

public class C : B{}

CBを直接継承し、Bを通してAのメンバーも受け継ぎます。ただし、これは多重継承ではなく、多段階の単一継承です。

8-2. C#で複数のインターフェースは実装できる?

C#のクラスや構造体は、複数のインターフェースを実装できます。

public class Sample : IDisposable, IComparable<Sample>{public void Dispose(){// リソースを解放する処理}

public int CompareTo(Sample? other){return 0;}

}

実装できるインターフェースを1つに制限する文法上のルールはありません。ただし、役割を増やしすぎるとクラスの責務が大きくなるため、設計上は適切な範囲に分割する必要があります。

8-3. インターフェースの多重継承はできる?

C#では、1つのインターフェースが複数のインターフェースを継承できます。

public interface IReadable{string Read();}

public interface IWritable{void Write(string value);}

public interface IFileAccess : IReadable, IWritable{}

IFileAccessを実装するクラスは、IReadableIWritableの両方の契約を満たす必要があります。

public class TextFileAccess : IFileAccess{public string Read(){return "ファイルの内容";}

public void Write(string value){Console.WriteLine($"書き込み: {value}");}

}

インターフェースの多重継承は可能ですが、クラスの多重継承とは異なります。主に複数の契約をまとめるための仕組みです。

8-4. デフォルトインターフェースメソッドは多重継承と同じ?

デフォルトインターフェースメソッドは、多重継承と似た部分を持っていますが、クラスの多重継承と同じではありません。

複数のインターフェースから実装済みメソッドを利用できるため、処理の再利用という点では多重継承に近く見えます。しかし、クラスのフィールドやコンストラクター、保護されたインスタンス状態などを複数の基底クラスから受け継ぐ仕組みではありません。

また、同じシグネチャのデフォルト実装が複数存在する場合、曖昧な状態のまま自動的にどちらかが選ばれるわけではありません。必要に応じて、実装クラス側で衝突を解決します。

デフォルトインターフェースメソッドは便利ですが、すべての共通処理をインターフェースへ集めるのではなく、主に契約を進化させるための機能として慎重に使用することが重要です。

8-5. Unityや.NET開発でも同じ考え方でよい?

Unityや一般的な.NETアプリケーションでも、C#の継承ルールは同じです。クラスの多重継承はできず、基底クラスは1つだけです。

Unityでは、多くのコンポーネントがMonoBehaviourを継承します。

public class PlayerController : MonoBehaviour{}

MonoBehaviourを継承しながら、別のクラスを追加で継承することはできません。そのため、複数の機能はインターフェースやコンポーネントとして分離する設計が適しています。

たとえば、プレイヤーの移動、攻撃、体力管理を別々のコンポーネントとして作成し、同じGameObjectへ追加できます。

public class PlayerMovement : MonoBehaviour{}

public class PlayerAttack : MonoBehaviour{}

public class PlayerHealth : MonoBehaviour{}

これはUnityのコンポーネント指向とも相性がよい設計です。

ASP.NET Coreなどの.NET開発では、インターフェースと依存性注入を組み合わせ、サービスの実装を分離する方法が一般的です。

9. C#で多重継承の代替手段を選ぶポイント

9-1. 共通処理を持たせたい場合

複数のクラスに共通処理を持たせたい場合は、その処理とクラス間の関係を確認します。

同じ種類のクラスが状態と処理を共有する場合は、抽象クラスが候補です。

public abstract class ReportBase{public DateTime CreatedAt { get; } = DateTime.Now;

public void ShowCreatedAt(){Console.WriteLine(CreatedAt);}public abstract void Generate();

}

一方、継承関係のない複数の型へ単純な補助処理を提供する場合は、拡張メソッドが適していることがあります。

状態を持つ独立した機能を再利用したい場合は、専用クラスとして分離し、コンポジションで利用する方法が向いています。

9-2. 複数の役割を持たせたい場合

1つのクラスに複数の役割を持たせたい場合は、インターフェースを利用します。

public interface IAuthenticatable{bool Authenticate(string password);}

public interface INotifiable{void Notify(string message);}

public class User : IAuthenticatable, INotifiable{public bool Authenticate(string password){return password == "sample";}

public void Notify(string message){Console.WriteLine(message);}

}

インターフェースは、クラスが「何であるか」よりも「何ができるか」を表現するのに適しています。

ただし、役割が多すぎるクラスは責務が集中している可能性があります。インターフェースを複数実装できるからといって、すべての機能を1つのクラスへ詰め込まないようにしましょう。

9-3. 既存クラスに機能を追加したい場合

既存クラスのソースコードを変更できない場合や、継承せずに便利な処理を追加したい場合は、拡張メソッドを検討します。

public static class EnumerableExtensions{public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source){return source is null || !source.Any();}}

利用例は次のとおりです。

List<int> numbers = new();

if (numbers.IsNullOrEmpty()){Console.WriteLine("要素がありません");}

ただし、拡張メソッドは既存クラスの内部状態へ自由にアクセスできません。新しい状態を追加したい場合や、動作を実行時に交換したい場合は、ラッパークラスやコンポジションのほうが適しています。

9-4. テストしやすい設計にしたい場合

テストしやすさを重視する場合は、インターフェースとコンポジションを組み合わせます。

たとえば、現在時刻を直接取得するクラスは、テスト結果が実行時刻に依存します。

public class MessageService{public string CreateMessage(){return $"現在時刻は{DateTime.Now}です";}}

時刻を提供する役割をインターフェースとして分離すると、テスト用の実装を渡せます。

public interface IClock{DateTime Now { get; }}

public class SystemClock : IClock{public DateTime Now => DateTime.Now;}

public class MessageService{private readonly IClock _clock;

public MessageService(IClock clock){_clock = clock;}public string CreateMessage(){return $"現在時刻は{_clock.Now}です";}

}

テストでは固定時刻を返す実装を使えます。

public class FixedClock : IClock{public DateTime Now { get; }

public FixedClock(DateTime now){Now = now;}

}

このように依存関係を外部から渡す設計にすると、実際の環境へ依存せずにテストできます。

9-5. 初心者が避けたい設計ミス

C#で多重継承の代替手段を使う際は、継承をコード再利用だけのために使わないことが重要です。

基底クラスの一部のメソッドを使いたいという理由だけで継承すると、不自然な「is-a」関係が生まれます。必要な機能を持つクラスを内部に保持するコンポジションを優先しましょう。

また、巨大なインターフェースを作ることも避けるべきです。

public interface IEverything{void Save();void Print();void SendEmail();void Validate();void Export();}

このようなインターフェースは、実装クラスに不要なメソッドまで要求する可能性があります。役割ごとに分割したほうが利用しやすくなります。

public interface ISavable{void Save();}

public interface IPrintable{void Print();}

public interface IValidatable{bool Validate();}

さらに、継承階層を深くしすぎると、処理の定義場所や変更の影響を追跡しにくくなります。継承は浅く保ち、独立した機能は小さなクラスへ分離することが大切です。

まとめ

C#では、1つのクラスが複数のクラスを継承する多重継承はできません。クラスが直接継承できる基底クラスは1つだけです。

クラスの多重継承が禁止されている背景には、ダイヤモンド問題、同名メンバーの衝突、継承関係の複雑化といった問題があります。C#では、これらの曖昧さを避けながら機能を組み合わせられる仕組みが用意されています。

複数の役割を持たせたい場合は、複数のインターフェースを実装します。共通の状態や基本処理を共有したい場合は、抽象クラスが適しています。独立した複数の機能を組み合わせたい場合は、コンポジションが有効です。

既存の型へ補助的な処理を追加するなら拡張メソッド、インターフェースに共通実装を持たせたい場合はデフォルトインターフェースメソッドも選択肢になります。

多重継承の代替手段を選ぶときは、単にコードを再利用できるかではなく、クラス同士がどのような関係にあるかを考えることが重要です。役割はインターフェース、共通基盤は抽象クラス、利用する機能はコンポジションという考え方を基準にすると、読みやすく変更に強いC#コードを設計できます。