C#継承の基礎を初心者向けにわかりやすく解説!使い方・メリット・注意点までまとめて理解

はじめに

C#でオブジェクト指向プログラミングを学ぶうえで、重要な仕組みの一つが「継承」です。継承を利用すると、既存のクラスが持つフィールドやプロパティ、メソッドなどを別のクラスに引き継ぎ、必要な機能だけを追加・変更できます。

たとえば、「動物」という共通クラスを作成し、その機能を引き継いだ「犬」や「猫」のクラスを作ることが可能です。名前や年齢などの共通情報は動物クラスにまとめ、それぞれの鳴き方だけを犬クラスや猫クラスで定義すれば、重複するコードを減らせます。

一方で、継承は使い方を誤るとクラス同士の関係が複雑になり、修正しにくいプログラムになることがあります。本記事では、C#における継承の基本構文から、basevirtualoverrideabstractなどの使い方、メリットや注意点まで初心者向けに解説します。

1. C#の継承とは?初心者向けに基本を解説

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

C#の継承とは、既存のクラスが持つ機能を、新しく作成するクラスへ引き継ぐ仕組みです。

たとえば、次のようなAnimalクラスがあるとします。

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

}

このAnimalクラスを継承してDogクラスを作成すると、Dogクラス側に同じコードを書かなくても、NameプロパティとEatメソッドを利用できます。

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

継承を利用することで、共通する機能を一つのクラスにまとめられます。派生クラスには、そのクラスだけが必要とする機能を追加できるため、コードの再利用性が高まります。

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

継承される側のクラスを「基底クラス」、継承する側のクラスを「派生クラス」と呼びます。

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

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

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

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

public class Animal{}

public class Dog : Animal{}

派生クラスは、基底クラスの機能を引き継ぎながら、独自の機能を持てます。

なお、C#のクラスが直接継承できるクラスは一つだけです。複数のクラスを同時に継承する「クラスの多重継承」には対応していません。複数の共通機能を表現したい場合は、インターフェースなどを組み合わせます。

1-3. 継承によって引き継げるメンバー・引き継げないメンバー

派生クラスでは、基底クラスに定義されている次のようなメンバーを利用できます。

  • フィールド

  • プロパティ

  • メソッド

  • イベント

  • インデクサー

ただし、実際に派生クラスからアクセスできるかどうかは、アクセス修飾子によって決まります。

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

protected int Age;private double Weight;

}

publicメンバーは、派生クラスだけでなくクラスの外部からもアクセスできます。

protectedメンバーは、クラスの外部から直接アクセスできませんが、そのクラス自身と派生クラスからアクセスできます。

privateメンバーは基底クラス内部でのみアクセスできます。派生クラスのオブジェクトにも基底クラスの内部状態として含まれますが、派生クラスから直接操作することはできません。

また、コンストラクターは派生クラスへそのまま継承されません。派生クラスで必要なコンストラクターを定義し、必要に応じて基底クラスのコンストラクターを呼び出します。

1-4. 「is-a関係」で考える継承の基本

継承を使うべきか判断するときは、「is-a関係」が成立するかを考えることが重要です。

「is-a関係」とは、「派生クラスは基底クラスの一種である」と自然に説明できる関係です。

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

  • 犬は動物の一種である

  • 猫は動物の一種である

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

このような関係は、継承で表現しやすいといえます。

一方で、「自動車はエンジンの一種である」という関係は成立しません。自動車はエンジンを「持っている」ため、これは「has-a関係」です。この場合は、継承ではなくフィールドやプロパティとして別のオブジェクトを持たせる方法が適しています。

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

public class Car{public Engine Engine { get; } = new Engine();}

コードを再利用したいという理由だけで継承を使うのではなく、クラス同士に自然な「is-a関係」があるかを確認しましょう。

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

2-1. コロンを使った継承の基本構文

C#でクラスを継承するときは、派生クラス名の後ろにコロンを書き、その後に基底クラス名を指定します。

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

具体例は次のとおりです。

public class Animal{}

public class Dog : Animal{}

このコードでは、DogクラスがAnimalクラスを継承しています。

派生クラスをさらに継承することも可能です。

public class Animal{}

public class Dog : Animal{}

public class GuideDog : Dog{}

ただし、継承階層が深くなるほど、どのクラスで何が定義されているのか分かりにくくなります。必要以上に継承を重ねず、役割を整理して設計することが大切です。

2-2. 基底クラスのフィールド・プロパティ・メソッドを利用する方法

派生クラスのインスタンスからは、アクセス可能な基底クラスのメンバーを通常のメンバーと同じように利用できます。

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

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

}

public class Dog : Animal{public void Introduce(){Age = 3;

    Console.WriteLine($"名前は{Name}です。");Console.WriteLine($"年齢は{Age}歳です。");}

}

Namepublicなので、派生クラスとクラス外部の両方からアクセスできます。

Ageprotectedなので、Dogクラス内部からはアクセスできますが、外部から直接アクセスすることはできません。

Dog dog = new Dog();

dog.Name = "ポチ";dog.Eat();dog.Introduce();

// dog.Age = 3;// protectedなのでクラス外部からはアクセスできない

2-3. 派生クラスに独自のメンバーを追加する方法

派生クラスには、基底クラスから引き継いだ機能に加えて、独自のフィールドやプロパティ、メソッドを追加できます。

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

public void Sleep(){Console.WriteLine($"{Name}が眠っています。");}

}

public class Dog : Animal{public string Breed { get; set; } = "";

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

}

Dogクラスのインスタンスは、基底クラスから引き継いだNameSleepに加えて、独自に定義したBreedBarkを利用できます。

2-4. 継承したクラスをインスタンス化して呼び出す方法

派生クラスも通常のクラスと同じように、new演算子を使ってインスタンス化します。

Dog dog = new Dog();

dog.Name = "ポチ";dog.Breed = "柴犬";

dog.Sleep();dog.Bark();

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

ポチが眠っています。ポチがワンワンと鳴きました。

派生クラスのインスタンスから、基底クラスのメンバーと派生クラス独自のメンバーの両方を呼び出せることがポイントです。

2-5. 初心者向けの簡単な継承サンプルコード

ここまでの内容をまとめたサンプルコードを確認してみましょう。

using System;

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

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

}

public class Student : Person{public int Grade { get; set; }

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

}

public class Program{public static void Main(){Student student = new Student();

    student.Name = "山田";student.Grade = 2;student.Introduce();student.Study();Console.WriteLine($"学年は{student.Grade}年です。");}

}

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

私の名前は山田です。山田が勉強しています。学年は2年です。

StudentクラスにはNameプロパティやIntroduceメソッドを定義していませんが、Personクラスを継承しているため利用できます。

3. コンストラクターとbaseキーワードの使い方

3-1. コンストラクターはそのまま継承されない

フィールドやプロパティ、メソッドとは異なり、基底クラスのコンストラクターは派生クラスへそのまま継承されません。

public class Person{public string Name { get; }

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

}

このPersonクラスを継承しただけでは、StudentクラスでStudent(string name)というコンストラクターを自動的に利用できるわけではありません。

派生クラス側にもコンストラクターを定義し、基底クラスのコンストラクターへ必要な値を渡す必要があります。

3-2. baseを使って基底クラスのコンストラクターを呼び出す方法

基底クラスのコンストラクターを明示的に呼び出すには、派生クラスのコンストラクターに: base(...)を指定します。

public class Person{public string Name { get; }

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

}

public class Student : Person{public int Grade { get; }

public Student(string name, int grade) : base(name){Grade = grade;}

}

Studentクラスのインスタンスは、次のように作成します。

Student student = new Student("佐藤", 3);

Console.WriteLine(student.Name);Console.WriteLine(student.Grade);

base(name)によって、受け取ったnamePersonクラスのコンストラクターに渡されます。

基底クラスに引数なしのコンストラクターがある場合、派生クラス側でbase()を省略すると、コンパイラが自動的に呼び出します。

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

public class Student : Person{public Student(){Console.WriteLine("Studentのコンストラクター");}}

一方、基底クラスに引数付きコンストラクターしか存在しない場合は、派生クラスからbaseを使って明示的に呼び出さなければコンパイルエラーになります。

3-3. baseで基底クラスのメソッドやプロパティへアクセスする方法

baseキーワードは、基底クラスのコンストラクターだけでなく、基底クラスのメソッドやプロパティへアクセスするときにも使用できます。

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

public class Dog : Animal{public override void Speak(){base.Speak();Console.WriteLine("犬がワンワンと鳴きます。");}}

この例では、DogクラスのSpeakメソッド内からbase.Speak()を呼び出しています。そのため、基底クラスの処理を実行した後に、派生クラス独自の処理が実行されます。

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

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

動物が鳴きます。犬がワンワンと鳴きます。

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

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

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

public class Dog : Animal{public Dog(){Console.WriteLine("Dogのコンストラクター");}}

次のコードを実行します。

Dog dog = new Dog();

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

AnimalのコンストラクターDogのコンストラクター

派生クラスの初期化処理を行う前に、基底クラス部分の初期化が必要になるため、この順番になります。

継承階層が複数ある場合は、最上位の基底クラスから順番にコンストラクターが実行されます。

4. メソッドのオーバーライドで処理を変更する方法

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

オーバーライドとは、基底クラスで定義されたメソッドの処理を、派生クラスで上書きする仕組みです。

たとえば、すべての動物には「鳴く」という共通の動作がありますが、実際の鳴き方は動物の種類によって異なります。

そのような場合に、基底クラスで共通のメソッドを定義し、派生クラスごとに処理を変更できます。

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

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

4-2. virtualとoverrideの基本的な使い方

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

派生クラスでは、同じシグネチャのメソッドにoverrideを付けて処理を定義します。

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

public class Dog : Animal{public override void Speak(){Console.WriteLine("犬がワンワンと鳴きます。");}}

public class Cat : Animal{public override void Speak(){Console.WriteLine("猫がニャーと鳴きます。");}}

呼び出し例は次のとおりです。

Dog dog = new Dog();Cat cat = new Cat();

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

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

犬がワンワンと鳴きます。猫がニャーと鳴きます。

基底クラス側でvirtual、派生クラス側でoverrideを使うことが基本です。

4-3. baseを使って元の処理も実行する方法

オーバーライドでは、基底クラスの処理を完全に置き換えるだけでなく、元の処理を実行してから派生クラス独自の処理を追加することも可能です。

public class Employee{public virtual void Work(){Console.WriteLine("業務を開始します。");}}

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

次のコードを実行します。

Engineer engineer = new Engineer();engineer.Work();

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

業務を開始します。プログラムを開発します。

base.Work()を呼び出すことで、基底クラスに定義された共通処理を再利用できます。

4-4. abstractメソッドを派生クラスで実装する方法

派生クラスごとに必ず異なる処理を実装させたい場合は、abstractを使用します。

abstractメソッドは処理本体を持たず、派生クラス側での実装を必須にするメソッドです。abstractメソッドを持つクラスには、クラス自体にもabstractを付けます。

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

public abstract void Speak();

}

派生クラスでは、overrideを付けてメソッドを実装します。

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

public class Cat : Animal{public override void Speak(){Console.WriteLine($"{Name}がニャーと鳴きます。");}}

抽象クラスは直接インスタンス化できません。

// Animal animal = new Animal();// abstractクラスなのでインスタンス化できない

派生クラスをインスタンス化して利用します。

Dog dog = new Dog{Name = "ポチ"};

dog.Speak();

abstractを利用すると、派生クラスに必要なメソッドの実装を強制できるため、実装漏れを防げます。

4-5. newによるメソッドの隠蔽とoverrideの違い

派生クラスでは、newキーワードを使って基底クラスのメソッドを隠すこともできます。

public class Animal{public void Speak(){Console.WriteLine("AnimalのSpeak");}}

public class Dog : Animal{public new void Speak(){Console.WriteLine("DogのSpeak");}}

newによる隠蔽では、変数の型によって呼び出されるメソッドが決まります。

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

dog.Speak();animal.Speak();

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

DogのSpeakAnimalのSpeak

一方、virtualoverrideを使用した場合は、変数の型ではなく、実際に生成されたインスタンスの型に応じて処理が選ばれます。

public class Animal{public virtual void Speak(){Console.WriteLine("AnimalのSpeak");}}

public class Dog : Animal{public override void Speak(){Console.WriteLine("DogのSpeak");}}

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

dog.Speak();animal.Speak();

実行結果は、どちらもDogクラスの処理になります。

DogのSpeakDogのSpeak

処理を多態的に切り替えたい場合は、通常、newではなくvirtualoverrideを使用します。newは基底クラスのメンバーを意図的に隠す場合に使いますが、動作を理解せずに使用すると混乱の原因になるため注意が必要です。

4-6. sealedでオーバーライドを禁止する方法

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

public sealed class FinalClass{}

次のようにFinalClassを継承しようとすると、コンパイルエラーになります。

// public class ChildClass : FinalClass// {// }

また、オーバーライドしたメソッドにsealedを付けると、それ以降の派生クラスで再度オーバーライドすることを禁止できます。

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

public class Dog : Animal{public sealed override void Speak(){Console.WriteLine("犬が鳴きます。");}}

public class GuideDog : Dog{// Dogでsealed overrideされているため変更できない// public override void Speak()// {// }}

クラスやメソッドの動作をこれ以上変更させたくない場合に、sealedを利用します。

5. C#で継承を使うメリット

5-1. 共通処理を再利用して重複コードを減らせる

継承の大きなメリットは、共通するコードを基底クラスへまとめられることです。

たとえば、DogCatBirdの各クラスにNameプロパティやEatメソッドをそれぞれ書くと、同じコードが何度も登場します。

共通部分をAnimalクラスへまとめれば、重複を減らせます。

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

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

}

public class Dog : Animal{}

public class Cat : Animal{}

public class Bird : Animal{}

コードの量が減るだけでなく、共通処理がどこに定義されているのかも分かりやすくなります。

5-2. 基底クラスを修正して複数の派生クラスへ反映できる

共通処理を基底クラスにまとめておけば、基底クラスを修正するだけで、その機能を利用する複数の派生クラスへ変更を反映できます。

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

public void Eat(){Console.WriteLine($"{Name}が食事を開始しました。");Console.WriteLine($"{Name}が食事を終了しました。");}

}

Eatメソッドを変更すれば、DogCatなどの派生クラスで個別に修正する必要がありません。

ただし、基底クラスの変更はすべての派生クラスへ影響する可能性があります。基底クラスを修正するときは、既存の派生クラスの動作が壊れないか確認することが重要です。

5-3. ポリモーフィズムによって異なるクラスを統一的に扱える

継承とオーバーライドを組み合わせると、異なる派生クラスを基底クラス型として統一的に扱えます。

public abstract class Animal{public abstract void Speak();}

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

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

複数の派生クラスを、Animal型の配列やリストへ格納できます。

List<Animal> animals = new List<Animal>{new Dog(),new Cat()};

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

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

ワンワンニャー

ループ内では、犬や猫を個別に判定していません。それでも実際のインスタンスに対応したSpeakメソッドが呼び出されます。

5-4. 機能の追加や変更に対応しやすくなる

共通の基底クラスを利用して設計すると、新しい派生クラスを追加しやすくなります。

たとえば、先ほどの動物クラスにBirdを追加する場合、Animalを継承して必要なメソッドを実装するだけです。

public class Bird : Animal{public override void Speak(){Console.WriteLine("ピヨピヨ");}}

既存の処理がAnimal型を対象に作られていれば、Birdを追加しても、その処理を大幅に書き換える必要はありません。

ただし、すべての機能追加に継承が適しているわけではありません。継承すると基底クラスと派生クラスの結び付きが強くなります。頻繁に組み替えたい機能や、「is-a関係」ではない機能については、オブジェクトをフィールドとして持たせるコンポジションも検討しましょう。

5-5. クラスの役割や共通点を整理しやすくなる

継承を適切に使うと、クラスの共通点と相違点を整理できます。

たとえば、従業員を表すクラスを次のように分類できます。

Employee├── FullTimeEmployee├── PartTimeEmployee└── ContractEmployee

氏名や社員番号など、すべての従業員に共通する情報はEmployeeへ定義します。給与計算など、雇用形態によって異なる処理は各派生クラスへ定義します。

このように設計すると、クラスごとの役割が明確になります。

一方で、継承階層を細かく作りすぎると、かえって全体像を把握しにくくなります。継承は必要最小限にとどめ、各クラスが一つの明確な役割を持つように設計することが大切です。

6. 継承とポリモーフィズムの関係

6-1. 派生クラスを基底クラス型の変数に代入する方法

派生クラスのインスタンスは、基底クラス型の変数へ代入できます。

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

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

public void Fetch(){Console.WriteLine("ボールを取ってきます。");}

}

DogAnimalの一種なので、次の代入が可能です。

Animal animal = new Dog();

これをアップキャストと呼びます。明示的なキャストを書く必要はありません。

ただし、変数の型がAnimalであるため、Animal型に定義されていないFetchメソッドは直接呼び出せません。

animal.Speak();

// animal.Fetch();// Animal型にはFetchが定義されていないため呼び出せない

派生クラス独自のメンバーを利用する必要がある場合は、パターンマッチングなどで型を確認します。

if (animal is Dog dog){dog.Fetch();}

型変換を行う前に、実際のインスタンスが目的の型であるか確認することが重要です。

6-2. 実際のインスタンスに応じて呼び出す処理が変わる仕組み

基底クラス型の変数に派生クラスのインスタンスを代入し、オーバーライドされたメソッドを呼び出すと、実際のインスタンスに応じた処理が実行されます。

public abstract class Payment{public abstract void Pay(int amount);}

public class CreditCardPayment : Payment{public override void Pay(int amount){Console.WriteLine($"クレジットカードで{amount}円を支払いました。");}}

public class CashPayment : Payment{public override void Pay(int amount){Console.WriteLine($"現金で{amount}円を支払いました。");}}

どちらの派生クラスも、Payment型の変数として扱えます。

Payment payment1 = new CreditCardPayment();Payment payment2 = new CashPayment();

payment1.Pay(3000);payment2.Pay(1500);

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

クレジットカードで3000円を支払いました。現金で1500円を支払いました。

変数の型はどちらもPaymentですが、実際に格納されているインスタンスが異なるため、呼び出される処理も変わります。この仕組みがポリモーフィズムです。

ポリモーフィズムを利用すると、呼び出し側が各派生クラスの具体的な種類を細かく意識せず、共通の方法で処理できます。新しい決済方法を追加する場合も、Paymentを継承したクラスを作成すれば、既存コードへの影響を抑えやすくなります。

まとめ

C#の継承は、既存クラスのフィールドやプロパティ、メソッドなどを派生クラスへ引き継ぐ仕組みです。クラス名の後ろにコロンと基底クラス名を書くことで継承できます。

継承を利用すると、共通処理を基底クラスへまとめてコードの重複を減らせます。また、virtualoverrideを使えば派生クラスごとに処理を変更でき、ポリモーフィズムによって異なるクラスを統一的に扱えます。

コンストラクターはそのまま継承されないため、必要に応じてbaseを使って基底クラスのコンストラクターを呼び出します。baseは、オーバーライドしたメソッドから基底クラスの元の処理を呼び出す場合にも利用できます。

継承を使うときは、基底クラスと派生クラスの間に「派生クラスは基底クラスの一種である」という自然な「is-a関係」が成立するかを確認しましょう。単にコードを再利用したいだけの場合や、部品として別の機能を持たせたい場合は、継承ではなくコンポジションのほうが適していることもあります。

まずは簡単な基底クラスと派生クラスを作り、メンバーの継承、baseによるコンストラクターの呼び出し、virtualoverrideによるオーバーライドを実際に試してみると、C#の継承を理解しやすくなります。