C#の抽象クラスとは?使い方・継承・インターフェースとの違いを初心者向けに解説
はじめに
C#で複数のクラスを設計していると、「共通する処理はまとめたいが、一部の処理は派生クラスごとに実装させたい」という場面があります。そのような場合に役立つのが、abstractキーワードを使って定義する抽象クラスです。
抽象クラスを利用すると、複数の派生クラスに共通するフィールド、プロパティ、メソッドをまとめながら、クラスごとに異なる処理の実装を強制できます。
この記事では、C#の抽象クラスとは何か、基本的な使い方や継承方法、通常のクラスやインターフェースとの違いを初心者向けに解説します。
1. C#の抽象クラス(abstract class)とは
C#の抽象クラスとは、ほかのクラスに継承されることを前提として設計されたクラスです。クラス名の前にabstractキーワードを付けて定義します。
基本的な構文は次のとおりです。
C#abstract class クラス名
{
// フィールド、プロパティ、メソッドなど
}
たとえば、動物を表す抽象クラスは次のように定義できます。
C#abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name}は眠っています。");
}
public abstract void Speak();
}
このAnimalクラスには、すべての動物に共通するNameプロパティとSleepメソッドがあります。
一方、鳴き方は動物によって異なるため、Speakメソッドには具体的な処理を書かず、abstractを付けています。このように、抽象クラスでは共通処理と個別処理のルールを1つのクラスにまとめられます。
1-1. 抽象クラスは継承を前提としたクラス
抽象クラスは、それ自体を完成したクラスとして使うのではなく、派生クラスの土台として使用します。
C#でクラスを継承するには、派生クラス名の後ろにコロンを記述し、継承元となる抽象クラスを指定します。
C#class Dog : Animal
{
public override void Speak()
{
Console.WriteLine($"{Name}はワンワンと鳴きます。");
}
}
DogクラスはAnimalクラスを継承しているため、NameプロパティとSleepメソッドを利用できます。
C#Dog dog = new Dog();
dog.Name = "ポチ";
dog.Sleep();
dog.Speak();
実行結果は次のようになります。
ポチは眠っています。
ポチはワンワンと鳴きます。
別の派生クラスも作成できます。
C#class Cat : Animal
{
public override void Speak()
{
Console.WriteLine($"{Name}はニャーと鳴きます。");
}
}
DogとCatは、どちらもAnimalの機能を引き継ぎながら、それぞれ異なるSpeakメソッドを実装しています。
C#Dog dog = new Dog { Name = "ポチ" };
Cat cat = new Cat { Name = "タマ" };
dog.Speak();
cat.Speak();
実行結果は次のとおりです。
ポチはワンワンと鳴きます。
タマはニャーと鳴きます。
このように抽象クラスを使うと、複数のクラスに共通する機能を再利用しながら、派生クラスごとの差異も表現できます。
1-2. 通常のクラスとの違い
抽象クラスと通常のクラスには、主に次のような違いがあります。
| 比較項目 | 抽象クラス | 通常のクラス |
|---|---|---|
abstractキーワード | 使用する | 使用しない |
| 直接インスタンス化 | できない | できる |
| 抽象メソッドの定義 | できる | できない |
| 主な用途 | 派生クラスの共通基盤 | 具体的なオブジェクトの生成 |
| 継承 | 継承されることを前提とする | 必須ではない |
通常のクラスは、次のように直接インスタンス化できます。
C#class User
{
public string Name { get; set; }
}
User user = new User();
一方、抽象クラスは直接インスタンス化できません。
C#abstract class Animal
{
}
次のコードはコンパイルエラーになります。
C#Animal animal = new Animal();
また、通常のクラスでは、処理内容を持たない抽象メソッドを定義できません。
C#class Animal
{
// 通常のクラスではabstractメソッドを定義できない
public abstract void Speak();
}
抽象メソッドを定義する場合、そのクラス自体もabstractにする必要があります。
C#abstract class Animal
{
public abstract void Speak();
}
なお、抽象クラスに記述できるのは抽象メソッドだけではありません。通常のメソッド、コンストラクター、フィールド、プロパティなども定義できます。
C#abstract class Employee
{
public string Name { get; }
protected Employee(string name)
{
Name = name;
}
public void ShowName()
{
Console.WriteLine($"名前:{Name}");
}
public abstract decimal CalculateSalary();
}
この点が、抽象クラスの大きな特徴です。共通処理はあらかじめ実装し、派生クラスごとに異なる処理だけを抽象メソッドとして定義できます。
1-3. 抽象クラスを直接インスタンス化できない理由
抽象クラスを直接インスタンス化できない理由は、クラスとしての実装が完成していない可能性があるためです。
たとえば、次のShapeクラスには、図形の面積を求めるGetAreaメソッドがあります。
C#abstract class Shape
{
public abstract double GetArea();
}
しかし、「図形」という情報だけでは面積を計算できません。円であれば半径が必要であり、長方形であれば幅と高さが必要です。
そのため、Shapeクラスは面積を求めるルールだけを定義し、具体的な計算処理は派生クラスに任せます。
C#class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
C#class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double GetArea()
{
return Width * Height;
}
}
利用する際は、完成した派生クラスをインスタンス化します。
C#Circle circle = new Circle(5);
Rectangle rectangle = new Rectangle(4, 6);
Console.WriteLine(circle.GetArea());
Console.WriteLine(rectangle.GetArea());
抽象クラスのインスタンスは作成できませんが、抽象クラス型の変数に派生クラスのインスタンスを代入することは可能です。
C#Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);
Console.WriteLine(shape1.GetArea());
Console.WriteLine(shape2.GetArea());
これはポリモーフィズムと呼ばれる仕組みです。変数の型を抽象クラスに統一しながら、実際のオブジェクトに応じた処理を実行できます。
複数の図形を同じコレクションで管理することも可能です。
C#List<Shape> shapes = new List<Shape>
{
new Circle(5),
new Rectangle(4, 6)
};
foreach (Shape shape in shapes)
{
Console.WriteLine($"面積:{shape.GetArea()}");
}
抽象クラスを使うことで、異なる種類のオブジェクトを共通の型として扱えるようになります。
1-4. 抽象クラスと抽象メソッドの関係
抽象メソッドとは、メソッド名、引数、戻り値などの形式だけを定義し、具体的な処理を派生クラスに任せるメソッドです。
抽象メソッドには処理本体を記述しません。
C#public abstract void Speak();
通常のメソッドのような波かっこは付けず、宣言の末尾にセミコロンを記述します。
抽象メソッドを持つクラスは、必ず抽象クラスとして定義しなければなりません。
C#abstract class Animal
{
public abstract void Speak();
}
抽象クラスを継承した通常のクラスは、原則としてすべての抽象メソッドを実装する必要があります。抽象メソッドを実装するときは、overrideキーワードを使用します。
C#class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}
Speakメソッドを実装しなければ、Dogクラスでコンパイルエラーが発生します。
C#class Dog : Animal
{
// Speakを実装していないためコンパイルエラー
}
ただし、派生クラスも抽象クラスとして定義する場合は、その時点で抽象メソッドを実装しなくても構いません。
C#abstract class Mammal : Animal
{
}
Mammalを継承する通常のクラスでは、最終的にSpeakメソッドを実装する必要があります。
抽象メソッドだけでなく、抽象プロパティも定義できます。
C#abstract class Product
{
public abstract decimal Price { get; }
public abstract string GetDescription();
}
派生クラスでは、抽象プロパティと抽象メソッドをそれぞれ実装します。
C#class Book : Product
{
public string Title { get; }
public override decimal Price { get; }
public Book(string title, decimal price)
{
Title = title;
Price = price;
}
public override string GetDescription()
{
return $"書籍名:{Title}、価格:{Price}円";
}
}
また、抽象クラスには抽象メンバーと通常のメンバーを共存させられます。
C#abstract class Payment
{
public void Start()
{
Console.WriteLine("決済処理を開始します。");
}
public abstract void Execute();
public void Finish()
{
Console.WriteLine("決済処理が完了しました。");
}
}
派生クラスでは、決済方法ごとに異なるExecuteメソッドだけを実装します。
C#class CreditCardPayment : Payment
{
public override void Execute()
{
Console.WriteLine("クレジットカードで決済します。");
}
}
C#CreditCardPayment payment = new CreditCardPayment();
payment.Start();
payment.Execute();
payment.Finish();
この設計では、処理の開始と終了は抽象クラスにまとめ、決済方法によって変わる部分だけを派生クラスに任せています。
抽象クラスはインターフェースと似ていますが、両者には役割の違いがあります。
| 比較項目 | 抽象クラス | インターフェース |
|---|---|---|
| 主な目的 | 共通の基底クラスを作る | クラスが持つ機能や契約を定める |
| フィールド | 定義できる | インスタンスフィールドは定義できない |
| コンストラクター | 定義できる | 定義できない |
| 実装済みメソッド | 定義できる | 定義できるが、主に契約の表現に使う |
| 継承・実装できる数 | 基底クラスは1つだけ | 複数のインターフェースを実装できる |
| アクセス修飾子 | 柔軟に指定できる | 契約として公開するメンバーが中心 |
| 状態の保持 | できる | インスタンスの状態は保持しない |
抽象クラスは、派生クラス間でデータや実装を共有したい場合に適しています。
C#abstract class Vehicle
{
protected int Speed { get; set; }
public void Stop()
{
Speed = 0;
Console.WriteLine("停止しました。");
}
public abstract void Move();
}
一方、インターフェースは、継承関係にかかわらず共通の機能を持たせたい場合に適しています。
C#interface IPrintable
{
void Print();
}
C#class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します。");
}
}
「共通する状態や処理を引き継がせたい」場合は抽象クラス、「特定の機能を実装することを約束させたい」場合はインターフェースを検討するとよいでしょう。
両方を組み合わせることもできます。
C#interface IChargeable
{
void Charge();
}
abstract class Vehicle
{
public abstract void Move();
}
class ElectricCar : Vehicle, IChargeable
{
public override void Move()
{
Console.WriteLine("電気自動車が走行します。");
}
public void Charge()
{
Console.WriteLine("バッテリーを充電します。");
}
}
この例では、ElectricCarはVehicleの一種として共通の継承関係を持ち、同時に充電できる機能をIChargeableインターフェースで表しています。
2. C#で抽象クラスを使うメリット
C#で抽象クラスを使う主なメリットは、共通処理を再利用しながら、派生クラスに必要な実装を強制できることです。
同じ種類のクラスを複数作る場合、抽象クラスを使わずに設計すると、各クラスに同じコードを繰り返し記述することがあります。
C#class Dog
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name}は眠っています。");
}
public void Speak()
{
Console.WriteLine("ワンワン");
}
}
class Cat
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name}は眠っています。");
}
public void Speak()
{
Console.WriteLine("ニャー");
}
}
このコードでは、NameプロパティとSleepメソッドが重複しています。クラスが増えるほど、同じコードを何度も管理しなければなりません。
抽象クラスを使えば、共通部分を1か所にまとめられます。
C#abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name}は眠っています。");
}
public abstract void Speak();
}
C#class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー");
}
}
共通処理の修正が必要になった場合も、抽象クラスだけを変更すれば済みます。コードの重複が減るため、保守性や可読性の向上につながります。
2-1. 派生クラスに共通のルールを持たせられる
抽象クラスの重要なメリットは、派生クラスに共通のルールを持たせられることです。
たとえば、さまざまな形式で通知を送信するクラスを考えてみましょう。メール通知、SMS通知、アプリ通知では送信方法が異なりますが、「送信する」という処理自体はすべての通知クラスに必要です。
C#abstract class Notification
{
public string Message { get; }
protected Notification(string message)
{
Message = message;
}
public abstract void Send();
}
メール通知クラスでは、メール送信の処理を実装します。
C#class EmailNotification : Notification
{
public EmailNotification(string message) : base(message)
{
}
public override void Send()
{
Console.WriteLine($"メールを送信しました:{Message}");
}
}
SMS通知クラスでは、SMS送信の処理を実装します。
C#class SmsNotification : Notification
{
public SmsNotification(string message) : base(message)
{
}
public override void Send()
{
Console.WriteLine($"SMSを送信しました:{Message}");
}
}
抽象クラスにSendメソッドを定義しているため、Notificationを継承する通常のクラスは送信処理を必ず実装しなければなりません。
C#List<Notification> notifications = new List<Notification>
{
new EmailNotification("会員登録が完了しました。"),
new SmsNotification("認証コードは1234です。")
};
foreach (Notification notification in notifications)
{
notification.Send();
}
新しい通知方法を追加する場合も、同じルールに従ってクラスを作成できます。
C#class AppNotification : Notification
{
public AppNotification(string message) : base(message)
{
}
public override void Send()
{
Console.WriteLine($"アプリ通知を送信しました:{Message}");
}
}
抽象メソッドによって必要な処理の実装漏れをコンパイル時に検出できるため、クラスごとの仕様のばらつきを防げます。
さらに、処理の流れそのものを抽象クラスで統一することも可能です。
C#abstract class DataImporter
{
public void Import()
{
Console.WriteLine("インポートを開始します。");
ReadData();
ValidateData();
SaveData();
Console.WriteLine("インポートが完了しました。");
}
protected abstract void ReadData();
protected virtual void ValidateData()
{
Console.WriteLine("共通の検証処理を実行します。");
}
protected abstract void SaveData();
}
この例では、Importメソッドが処理全体の順番を管理しています。データの読み込み方法と保存方法は派生クラスに任せ、検証処理には共通の実装を用意しています。
C#class CsvImporter : DataImporter
{
protected override void ReadData()
{
Console.WriteLine("CSVファイルを読み込みます。");
}
protected override void SaveData()
{
Console.WriteLine("CSVデータを保存します。");
}
}
C#CsvImporter importer = new CsvImporter();
importer.Import();
virtualメソッドは、抽象メソッドとは異なり、基底クラス側に標準の処理を記述できます。派生クラスは必要な場合だけoverrideして処理を変更します。
C#protected virtual void ValidateData()
{
Console.WriteLine("共通の検証処理を実行します。");
}
抽象メソッドと仮想メソッドの違いは次のとおりです。
| 比較項目 | abstractメソッド | virtualメソッド |
|---|---|---|
| 基底クラスでの処理本体 | ない | ある |
| 派生クラスでのオーバーライド | 原則必須 | 任意 |
| 主な用途 | 実装を派生クラスに強制する | 標準処理を必要に応じて変更させる |
抽象クラスでは、共通処理、抽象メソッド、仮想メソッドを組み合わせることで、柔軟かつ一貫性のあるクラス設計ができます。
ただし、すべての共通処理を抽象クラスに集めると、基底クラスの責任が大きくなりすぎることがあります。抽象クラスを使う際は、派生クラス同士に明確な共通関係があるかを確認することが大切です。
「AはBの一種である」と自然に表現できる場合は、抽象クラスが適しています。たとえば、「犬は動物の一種」「円は図形の一種」「クレジットカード決済は決済方法の一種」といった関係です。
反対に、単に同じ機能を持たせたいだけで明確な親子関係がない場合は、インターフェースのほうが適している可能性があります。
まとめ
C#の抽象クラスは、複数の派生クラスに共通するデータや処理をまとめるためのクラスです。abstract classを使って定義し、継承されることを前提としているため、抽象クラスを直接インスタンス化することはできません。
抽象クラスには、処理を実装した通常のメソッドだけでなく、派生クラスに実装を任せる抽象メソッドも定義できます。派生クラスでは、抽象メソッドをoverrideして具体的な処理を記述します。
抽象クラスを活用する主なメリットは、共通コードの重複を減らし、派生クラスに統一されたルールを持たせられることです。また、抽象クラス型の変数を使えば、異なる派生クラスを共通の型として扱えます。
共通の状態や実装を引き継がせたい場合は抽象クラス、継承関係にかかわらず特定の機能を実装させたい場合はインターフェースが適しています。両者の違いを理解し、クラス同士の関係や共有したい内容に応じて使い分けましょう。

