C#のabstractとは?抽象クラス・抽象メソッドの使い方とinterfaceとの違いを初心者向けに解説

はじめに

C#で複数のクラスを設計していると、「共通する処理はまとめたいが、クラスごとに異なる処理は個別に実装させたい」という場面があります。そのような場合に役立つのが、abstractキーワードを使った抽象クラスと抽象メソッドです。

抽象クラスを利用すると、派生クラスに共通のフィールドやメソッドを提供しながら、必要な処理の実装を強制できます。重複コードを減らし、クラスごとの違いを明確に表現できるため、オブジェクト指向プログラミングにおける重要な仕組みの一つです。

この記事では、C#のabstractの意味や基本構文、抽象クラス・抽象メソッドの使い方をサンプルコード付きで解説します。interfacevirtualoverridesealedとの違いや、実際の設計で使う際の注意点も確認していきましょう。

1. C#のabstractとは

1-1. abstractキーワードの役割

C#のabstractは、クラスやメンバーが「抽象的な定義」であることを示すキーワードです。

クラスにabstractを付けると、そのクラスは抽象クラスになります。抽象クラスは、ほかのクラスから継承されることを前提としたクラスです。

メソッドやプロパティなどにabstractを付けると、派生クラスで具体的な実装を行う必要がある抽象メンバーになります。

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

この例では、Animalが抽象クラス、Speakが抽象メソッドです。

Animalクラス自身は、動物がどのように鳴くかを決めていません。犬や猫などの派生クラスが、それぞれの鳴き方を実装します。

1-2. 抽象化が必要になる理由

抽象化とは、複数のオブジェクトに共通する特徴を取り出し、重要な部分だけを上位の概念として表現することです。

たとえば、犬と猫には次のような共通点があります。

  • 名前を持つ

  • 食事をする

  • 鳴く

一方で、鳴き方は動物ごとに異なります。

この場合、「名前を持つ」「食事をする」といった共通部分は基底クラスにまとめ、「鳴く」という処理だけを派生クラスに実装させることができます。

抽象化を行わず、犬クラスと猫クラスの両方に同じ処理を書くと、重複コードが増えてしまいます。共通処理を抽象クラスにまとめれば、修正箇所を減らし、設計上のルールも明確にできます。

1-3. abstractを使用できるクラスとメンバー

abstractは、主に次の要素に指定できます。

  • クラス

  • メソッド

  • プロパティ

  • インデクサー

  • イベント

抽象メソッドや抽象プロパティなどの抽象メンバーは、抽象クラスの中に定義します。

C#
public abstract class Sample
{
public abstract void Execute();

public abstract string Name { get; }

public abstract event EventHandler? Completed;
}

抽象メンバーは、基本的に派生クラスでオーバーライドされることを前提としています。

1-4. abstractクラスはインスタンス化できない

抽象クラスは、new演算子を使って直接インスタンス化できません。

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

// コンパイルエラー
Animal animal = new Animal();

抽象クラスには、実装されていない抽象メンバーが含まれる可能性があります。そのため、単独で完全なオブジェクトとして扱うことはできません。

抽象クラスを利用するには、具体的な派生クラスを作成します。

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

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

変数の型として抽象クラスを指定することは可能です。実際に生成するオブジェクトには、抽象メンバーを実装した派生クラスを指定します。

2. C#の抽象クラスの基本的な使い方

2-1. 抽象クラスを定義する構文

抽象クラスは、classの前にabstractを指定して定義します。

C#
アクセス修飾子 abstract class クラス名
{
}

具体的には、次のように記述します。

C#
public abstract class Animal
{
}

抽象クラスだからといって、必ず抽象メンバーを定義しなければならないわけではありません。

次のように、抽象メンバーを持たない抽象クラスも定義できます。

C#
public abstract class BaseService
{
public void Log(string message)
{
Console.WriteLine(message);
}
}

このような設計は、基底クラスを直接インスタンス化させず、必ず派生クラスを通して利用させたい場合に使われます。

2-2. 抽象クラスを継承する方法

抽象クラスの継承方法は、通常のクラスと同じです。クラス名の後ろにコロンを付け、継承する抽象クラスを指定します。

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

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

抽象クラスの抽象メンバーを実装するときは、overrideキーワードを使用します。

具体的なクラスとして定義する派生クラスは、継承したすべての抽象メンバーを実装しなければなりません。

派生クラスもabstractとして定義する場合は、抽象メンバーの実装を、さらに下位の派生クラスへ先送りできます。

C#
public abstract class Mammal : Animal
{
}

2-3. 抽象クラスに通常のメソッドやフィールドを定義する

抽象クラスには、抽象メンバーだけでなく、通常のメソッドやフィールドも定義できます。

C#
public abstract class Animal
{
private readonly string name;

protected Animal(string name)
{
this.name = name;
}

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

public abstract void Speak();
}

Introduceメソッドには処理本体があり、すべての派生クラスでそのまま利用できます。一方、Speakメソッドは抽象メソッドなので、派生クラスごとに実装します。

抽象クラスが通常のフィールドやメソッドを持てる点は、インターフェースとの大きな違いです。

2-4. 抽象クラスのコンストラクターを利用する

抽象クラスは直接インスタンス化できませんが、コンストラクターを定義できます。

抽象クラスのコンストラクターは、派生クラスのインスタンスを生成するときに呼び出されます。

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

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

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

Dogのインスタンスを生成すると、Dogのコンストラクターからbase(name)を通して、Animalのコンストラクターが呼ばれます。

C#
Dog dog = new Dog("ポチ");
Console.WriteLine(dog.Name);

抽象クラスのコンストラクターは、外部から直接呼び出す必要がないため、protectedにするのが一般的です。

2-5. 抽象クラスを使ったサンプルコード

次の例では、従業員を表すEmployee抽象クラスを定義しています。

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

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

public void PrintName()
{
Console.WriteLine($"従業員名: {Name}");
}

public abstract decimal CalculateSalary();
}

public class FullTimeEmployee : Employee
{
private readonly decimal monthlySalary;

public FullTimeEmployee(string name, decimal monthlySalary)
: base(name)
{
this.monthlySalary = monthlySalary;
}

public override decimal CalculateSalary()
{
return monthlySalary;
}
}

public class HourlyEmployee : Employee
{
private readonly decimal hourlyRate;
private readonly int workingHours;

public HourlyEmployee(
string name,
decimal hourlyRate,
int workingHours)
: base(name)
{
this.hourlyRate = hourlyRate;
this.workingHours = workingHours;
}

public override decimal CalculateSalary()
{
return hourlyRate * workingHours;
}
}

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

C#
Employee fullTime =
new FullTimeEmployee("田中", 300000m);

Employee hourly =
new HourlyEmployee("佐藤", 1500m, 160);

fullTime.PrintName();
Console.WriteLine(fullTime.CalculateSalary());

hourly.PrintName();
Console.WriteLine(hourly.CalculateSalary());

名前の表示は共通処理として抽象クラスにまとめ、給与計算だけを派生クラスごとに実装しています。

3. C#の抽象メソッドの基本的な使い方

3-1. 抽象メソッドを定義する構文

抽象メソッドは、戻り値の型の前にabstractを指定して定義します。

C#
アクセス修飾子 abstract 戻り値の型 メソッド名();

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

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

抽象メソッドは派生クラスから実装される必要があるため、privateにはできません。一般的にはpublicまたはprotectedを指定します。

3-2. overrideで抽象メソッドを実装する

抽象メソッドを派生クラスで実装するときは、overrideを指定します。

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

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

public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}

メソッド名、戻り値の型、引数などは、基底クラスで宣言された抽象メソッドに対応させる必要があります。

C#
Shape shape = new Circle(5);
Console.WriteLine(shape.CalculateArea());

変数の型がShapeであっても、実行時にはCircleCalculateAreaメソッドが呼び出されます。

3-3. 抽象メソッドに処理本体を記述できない理由

抽象メソッドは、「どのような操作が必要か」だけを宣言し、「具体的に何をするか」は派生クラスに任せる仕組みです。

そのため、抽象メソッド自体には処理本体を記述しません。

C#
public abstract void Execute();

次のように処理本体を書くことはできません。

C#
// コンパイルエラー
public abstract void Execute()
{
Console.WriteLine("実行");
}

基底クラス側で標準的な処理を用意しつつ、必要に応じて派生クラスに変更させたい場合は、abstractではなくvirtualを使用します。

3-4. 抽象メソッドを実装しない場合のエラー

具体的な派生クラスが抽象メソッドを実装しない場合、コンパイルエラーになります。

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

// Speakを実装していないためコンパイルエラー
public class Dog : Animal
{
}

正しく実装するには、overrideを指定します。

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

実装を後続のクラスへ任せる場合は、派生クラス自身も抽象クラスとして宣言します。

C#
public abstract class DogBase : Animal
{
}

3-5. 抽象プロパティの定義と実装

抽象クラスには、抽象プロパティも定義できます。

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

public abstract decimal Price { get; set; }
}

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

C#
public class Book : Product
{
public override string Name { get; }

public override decimal Price { get; set; }

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

抽象プロパティは、派生クラスが必ず持つべき情報を定義したい場合に便利です。

基底クラスでgetだけを宣言した場合、派生クラスでも少なくとも読み取りが可能なプロパティとして実装します。getsetの両方を宣言した場合は、両方に対応する必要があります。

3-6. 抽象メソッドと抽象プロパティを使ったサンプルコード

次の例では、通知機能を抽象クラスとして表現しています。

C#
public abstract class Notification
{
public abstract string Destination { get; }

public void PrintStartMessage()
{
Console.WriteLine(
$"{Destination}への通知を開始します。");
}

public abstract void Send(string message);
}

public class EmailNotification : Notification
{
public override string Destination { get; }

public EmailNotification(string emailAddress)
{
Destination = emailAddress;
}

public override void Send(string message)
{
Console.WriteLine(
$"{Destination}にメールを送信: {message}");
}
}

public class SmsNotification : Notification
{
public override string Destination { get; }

public SmsNotification(string phoneNumber)
{
Destination = phoneNumber;
}

public override void Send(string message)
{
Console.WriteLine(
$"{Destination}にSMSを送信: {message}");
}
}

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

C#
Notification notification =
new EmailNotification("user@example.com");

notification.PrintStartMessage();
notification.Send("登録が完了しました。");

通知開始時の処理は共通化し、送信先と送信方法は派生クラスごとに変更しています。

4. abstractクラスを使うメリット

4-1. 派生クラスに共通の実装を提供できる

抽象クラスには、処理本体を持つ通常のメソッドを定義できます。

たとえば、複数のサービスで同じログ出力処理を使う場合、抽象クラスにまとめられます。

C#
public abstract class ServiceBase
{
protected void Log(string message)
{
Console.WriteLine(
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
}
}

派生クラスは同じログ処理を繰り返し実装する必要がありません。共通処理を修正するときも、抽象クラスだけを変更すれば済みます。

4-2. 派生クラスに必要な処理の実装を強制できる

抽象メソッドを定義すると、具体的な派生クラスにその実装を強制できます。

C#
public abstract class FileParser
{
public abstract object Parse(string content);
}

CSV用、JSON用、XML用などの派生クラスを作る場合、すべてのクラスがParseメソッドを実装しなければなりません。

実装漏れはコンパイル時に検出されるため、「特定のクラスだけ必要な処理を実装していなかった」という問題を防ぎやすくなります。

4-3. 重複コードを減らして保守性を高められる

派生クラス間で共通する処理を抽象クラスにまとめると、同じコードを何度も書く必要がなくなります。

重複コードが少なくなると、次のような効果があります。

  • 修正箇所を減らせる

  • クラスごとの処理のばらつきを防げる

  • テスト対象を整理しやすくなる

  • コードの意図を理解しやすくなる

ただし、見た目が似ているという理由だけで無理に共通化すると、抽象クラスが複雑になる可能性があります。本当に同じ責務を持つ処理かどうかを判断することが大切です。

4-4. ポリモーフィズムを活用できる

抽象クラス型の変数には、その抽象クラスを継承したオブジェクトを代入できます。

C#
Animal[] animals =
{
new Dog(),
new Cat()
};

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

DogCatを同じAnimal型として扱いながら、実行時にはそれぞれのSpeakメソッドを呼び出せます。

このように、同じ操作で異なる処理を実行できる仕組みをポリモーフィズムといいます。

4-5. 共通仕様を保ちながらクラスごとの差を表現できる

抽象クラスを使うと、すべての派生クラスに共通する仕様を保ちながら、必要な部分だけを変更できます。

たとえば、注文処理に次の共通手順があるとします。

  1. 入力内容を検証する

  2. 決済する

  3. 注文を保存する

  4. 完了通知を送る

このうち、決済方法だけが注文の種類によって異なる場合、全体の流れは基底クラスに定義し、決済処理だけを抽象メソッドにできます。

これにより、派生クラスが共通の処理手順を崩すことなく、個別の動作を実装できます。

5. abstractクラスとinterfaceの違い

5-1. 抽象クラスとインターフェースの共通点

抽象クラスとインターフェースは、どちらも複数のクラスに共通する仕様を定義するために使います。

主な共通点は次のとおりです。

  • 直接利用する具体的な処理と、派生先に任せる処理を分けられる

  • 実装クラスを共通の型として扱える

  • ポリモーフィズムを利用できる

  • 実装クラスに特定のメンバーを持たせられる

ただし、抽象クラスは「共通する基盤」を表現するのに向いており、インターフェースは「実行できる役割や能力」を表現するのに向いています。

5-2. 継承できる数の違い

C#では、クラスが継承できる基底クラスは一つだけです。

C#
public class Dog : Animal
{
}

二つ以上のクラスを同時に継承することはできません。

一方、インターフェースは複数実装できます。

C#
public class SmartPhone :
ICallable,
ICamera,
IInternetDevice
{
}

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

5-3. フィールドやコンストラクターを持てるかの違い

抽象クラスは、通常のクラスと同様にインスタンスフィールドやコンストラクターを持てます。

C#
public abstract class User
{
private readonly int id;

protected User(int id)
{
this.id = id;
}
}

インターフェースは、クラスのインスタンス状態を保存するためのインスタンスフィールドや、実装クラスを初期化するためのコンストラクターを持ちません。

共通の状態や初期化処理を提供したい場合は、抽象クラスが向いています。

5-4. 共通処理を実装する方法の違い

抽象クラスには、通常のメソッドとして共通処理を自由に実装できます。

C#
public abstract class Report
{
public void PrintHeader()
{
Console.WriteLine("レポート");
}

public abstract void PrintBody();
}

現在のC#では、インターフェースにもデフォルト実装を持つメンバーを定義できます。

C#
public interface ILogger
{
void Log(string message)
{
Console.WriteLine(message);
}
}

ただし、インターフェースのデフォルト実装は、既存の実装クラスに影響を与えず機能を追加する用途などで使われます。共有する状態やコンストラクターを含む共通基盤を作る場合は、抽象クラスのほうが自然です。

5-5. アクセス修飾子の扱いの違い

抽象クラスのメンバーには、publicprotectedinternalなど、目的に応じたアクセス修飾子を指定できます。

C#
public abstract class BaseClass
{
protected abstract void ExecuteCore();

public void Execute()
{
ExecuteCore();
}
}

抽象メンバーは派生クラスからオーバーライドされる必要があるため、privateにはできません。

インターフェースで外部に公開する契約メンバーは、基本的にpublicとして扱われます。現在のC#では、デフォルト実装を補助するための非公開メンバーなども記述できますが、初心者のうちは「インターフェースの契約は公開された操作を表す」と考えると理解しやすいでしょう。

5-6. 状態を保持できるかの違い

抽象クラスはインスタンスフィールドを持てるため、オブジェクトごとの状態を保持できます。

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

protected Account(decimal initialBalance)
{
Balance = initialBalance;
}

protected void AddBalance(decimal amount)
{
Balance += amount;
}
}

インターフェースは、実装クラスのインスタンス状態を保持するための場所ではありません。インターフェースにプロパティを宣言することはできますが、実際の値は実装クラス側で保持します。

C#
public interface IAccount
{
decimal Balance { get; }
}

5-7. abstractクラスとinterfaceの違いを比較表で確認

比較項目抽象クラスインターフェース
直接インスタンス化できないできない
継承・実装できる数基底クラスは一つ複数実装できる
インスタンスフィールド定義できる定義できない
コンストラクター定義できる定義できない
通常メソッドの実装定義できるデフォルト実装が可能
抽象的なメンバー定義できる契約として宣言できる
状態の共有可能基本的に不可
主な用途共通基盤や同種のクラスを表す能力、役割、契約を表す
アクセス制御柔軟に指定できる契約メンバーは基本的に公開される

5-8. 抽象クラスを選ぶべきケース

次のような場合は、抽象クラスが適しています。

  • 派生クラス同士が同じ種類のオブジェクトである

  • 共通のフィールドや状態を持たせたい

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

  • 多くの共通処理を再利用したい

  • 派生クラスに特定の処理だけを実装させたい

  • protectedメンバーを使って派生クラス向けの機能を提供したい

たとえば、DogCatがどちらもAnimalであるという関係は、抽象クラスで表現しやすい関係です。

5-9. interfaceを選ぶべきケース

次のような場合は、インターフェースが適しています。

  • 継承関係にないクラスへ共通の機能を持たせたい

  • 一つのクラスに複数の役割を持たせたい

  • 実装内容ではなく、公開する操作だけを決めたい

  • クラス同士の依存関係を弱くしたい

  • テスト用の実装へ差し替えやすくしたい

たとえば、プリンターとファイル出力機能の両方が「印刷できる」という能力を持つ場合、IPrintableのようなインターフェースで表現できます。

5-10. 抽象クラスとinterfaceを併用するケース

抽象クラスとインターフェースは、どちらか一方だけを選ばなければならないわけではありません。

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

public abstract class PaymentBase : ILoggable
{
public void Log(string message)
{
Console.WriteLine($"決済ログ: {message}");
}

public abstract void Pay(decimal amount);
}

public class CreditCardPayment : PaymentBase
{
public override void Pay(decimal amount)
{
Log($"{amount:N0}円をカードで決済しました。");
}
}

この例では、ILoggableがログ出力という役割を定義し、PaymentBaseが決済クラスに共通する実装を提供しています。

インターフェースで外部向けの契約を定義し、抽象クラスで標準的な実装を提供する設計も可能です。

6. abstractとvirtual・override・sealedの違い

6-1. abstractとvirtualの違い

abstractvirtualは、どちらも派生クラスでオーバーライドできるメンバーに使用します。ただし、基底クラスに実装があるかどうかが異なります。

abstractメソッドには実装がありません。

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

具体的な派生クラスは、必ず実装する必要があります。

virtualメソッドには、基底クラス側の標準実装があります。

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

派生クラスは必要な場合だけオーバーライドします。

6-2. abstractメソッドとvirtualメソッドの使い分け

派生クラスごとに処理内容が必ず異なり、基底クラス側で適切な標準実装を用意できない場合は、abstractを使います。

C#
public abstract double CalculateArea();

すべての派生クラスで利用できる標準処理があり、一部のクラスだけ変更できるようにしたい場合は、virtualを使います。

C#
public virtual decimal CalculateFee()
{
return 500m;
}

判断基準は、「基底クラスだけで意味のある実装を提供できるか」です。

6-3. overrideが必要になるケース

overrideは、基底クラスでabstractまたはvirtualとして定義されたメンバーを、派生クラスで再定義するときに使用します。

C#
public abstract class BaseClass
{
public abstract void MethodA();

public virtual void MethodB()
{
Console.WriteLine("Base");
}
}

public class DerivedClass : BaseClass
{
public override void MethodA()
{
Console.WriteLine("MethodA");
}

public override void MethodB()
{
Console.WriteLine("Derived");
}
}

abstractメンバーは具体的な派生クラスで実装が必須です。virtualメンバーのオーバーライドは任意です。

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

オーバーライドしたメソッドを、さらに下位の派生クラスから変更させたくない場合は、sealed overrideを指定します。

C#
public class BaseClass
{
public virtual void Execute()
{
Console.WriteLine("Base");
}
}

public class MiddleClass : BaseClass
{
public sealed override void Execute()
{
Console.WriteLine("Middle");
}
}

public class FinalClass : MiddleClass
{
// Executeはオーバーライドできない
}

sealed overrideは、継承自体は許可しながら、特定のメソッドの動作だけを固定したい場合に使います。

6-5. newによるメンバーの隠蔽との違い

newキーワードを使うと、基底クラスのメンバーをオーバーライドするのではなく、同じ名前の新しいメンバーで隠蔽できます。

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

public class DerivedClass : BaseClass
{
public new void Show()
{
Console.WriteLine("Derived");
}
}

呼び出されるメソッドは、変数の宣言型によって変わります。

C#
BaseClass baseValue = new DerivedClass();
DerivedClass derivedValue = new DerivedClass();

baseValue.Show(); // Base
derivedValue.Show(); // Derived

一方、overrideでは実際のオブジェクト型に応じてメソッドが選択されます。

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

public class DerivedClass : BaseClass
{
public override void Show()
{
Console.WriteLine("Derived");
}
}

BaseClass value = new DerivedClass();
value.Show(); // Derived

ポリモーフィズムを利用する目的であれば、通常はnewによる隠蔽ではなく、virtualoverrideを使用します。

7. abstractを使った実践的なサンプル

7-1. 図形クラスで面積計算を共通化する例

図形には名前や面積を表示する処理がありますが、面積の計算方法は図形ごとに異なります。

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

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

public abstract double CalculateArea();

public void PrintArea()
{
Console.WriteLine(
$"{Name}の面積: {CalculateArea():F2}");
}
}

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

public Circle(double radius)
: base("円")
{
Radius = radius;
}

public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}

public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }

public Rectangle(double width, double height)
: base("長方形")
{
Width = width;
Height = height;
}

public override double CalculateArea()
{
return Width * Height;
}
}

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

C#
Shape[] shapes =
{
new Circle(3),
new Rectangle(4, 5)
};

foreach (Shape shape in shapes)
{
shape.PrintArea();
}

PrintAreaを共通化し、CalculateAreaだけを派生クラスごとに実装しています。

7-2. 支払い方法ごとの処理を実装する例

支払い処理では、金額の検証や完了メッセージは共通でも、実際の決済方法は異なります。

C#
public abstract class PaymentMethod
{
public void Process(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(amount),
"金額は0より大きい値にしてください。");
}

Pay(amount);

Console.WriteLine(
$"{amount:N0}円の支払いが完了しました。");
}

protected abstract void Pay(decimal amount);
}

public class CreditCardPayment : PaymentMethod
{
protected override void Pay(decimal amount)
{
Console.WriteLine(
"クレジットカード会社へ決済を要求します。");
}
}

public class BankTransferPayment : PaymentMethod
{
protected override void Pay(decimal amount)
{
Console.WriteLine(
"銀行振込の受付情報を登録します。");
}
}

外部からはProcessだけを呼び出し、個別の決済処理はPayへ分離しています。

7-3. 共通処理と個別処理を分離する例

処理全体の流れを基底クラスに定義し、一部だけを派生クラスに任せる設計をテンプレートメソッドパターンと呼びます。

C#
public abstract class DataImporter
{
public void Import(string path)
{
string content = ReadFile(path);
Validate(content);
Save(content);

Console.WriteLine("取り込みが完了しました。");
}

private string ReadFile(string path)
{
return File.ReadAllText(path);
}

protected virtual void Validate(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new InvalidOperationException(
"データが空です。");
}
}

protected abstract void Save(string content);
}

共通のファイル読み込みや入力検証は基底クラスで行い、保存方法だけを抽象メソッドにしています。

C#
public class CustomerImporter : DataImporter
{
protected override void Save(string content)
{
Console.WriteLine(
"顧客データとして保存します。");
}
}

処理の順序を統一しながら、必要な部分だけを差し替えられます。

7-4. 抽象クラス型の変数で派生クラスを扱う例

抽象クラス型のコレクションを使うと、異なる派生クラスをまとめて処理できます。

C#
public abstract class MessageSender
{
public abstract void Send(string message);
}

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

public class SlackSender : MessageSender
{
public override void Send(string message)
{
Console.WriteLine($"Slack送信: {message}");
}
}
C#
List<MessageSender> senders =
[
new EmailSender(),
new SlackSender()
];

foreach (MessageSender sender in senders)
{
sender.Send("システムを更新しました。");
}

呼び出し側は、具体的な送信方法を意識せずにSendを実行できます。

7-5. interfaceと組み合わせて設計する例

インターフェースで外部向けの契約を定義し、抽象クラスで共通実装を提供することもできます。

C#
public interface IReportExporter
{
void Export(string content);
}

public abstract class ReportExporterBase :
IReportExporter
{
public void Export(string content)
{
Validate(content);
ExportCore(content);
WriteLog();
}

private static void Validate(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentException(
"出力内容が空です。",
nameof(content));
}
}

protected abstract void ExportCore(string content);

private static void WriteLog()
{
Console.WriteLine("出力履歴を記録しました。");
}
}

public class PdfReportExporter :
ReportExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine("PDF形式で出力します。");
}
}

public class CsvReportExporter :
ReportExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine("CSV形式で出力します。");
}
}

利用側はIReportExporterに依存できます。

C#
IReportExporter exporter =
new PdfReportExporter();

exporter.Export("売上レポート");

インターフェースに依存することで実装を差し替えやすくし、抽象クラスによって実装クラス間の重複を減らしています。

8. abstractを使用する際の注意点

8-1. 抽象クラスを直接newできない

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

C#
public abstract class Animal
{
}

// コンパイルエラー
Animal animal = new Animal();

必ず具体的な派生クラスを作成します。

C#
public class Dog : Animal
{
}

Animal animal = new Dog();

抽象クラス型の変数や引数を使うことはできますが、実際に生成するオブジェクトは具象クラスである必要があります。

8-2. abstractメンバーはabstractクラス内に定義する

抽象メソッドなどの抽象メンバーは、通常の具象クラスには定義できません。

C#
// コンパイルエラー
public class Animal
{
public abstract void Speak();
}

クラス側にもabstractを付けます。

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

未実装のメンバーを持つクラスは、それ自体も未完成な型として扱う必要があるためです。

8-3. 派生クラスではすべての抽象メンバーを実装する

具象クラスとして定義する派生クラスは、継承したすべての抽象メンバーを実装しなければなりません。

C#
public abstract class Sample
{
public abstract void MethodA();
public abstract void MethodB();
}
C#
public class ConcreteSample : Sample
{
public override void MethodA()
{
}

public override void MethodB()
{
}
}

一つでも未実装の抽象メンバーが残っていると、コンパイルエラーになります。

ただし、派生クラス自身をabstractにする場合は、実装をさらに下位のクラスへ任せられます。

8-4. 多重継承ができない

C#のクラスは、複数の基底クラスを同時に継承できません。抽象クラスも同様です。

C#
// このような複数クラスの継承はできない
// public class Sample : BaseA, BaseB

すでに別のクラスを継承している場合、新たに抽象クラスを継承することはできません。

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

8-5. 抽象クラスに役割を持たせすぎない

共通処理をまとめられるからといって、関係の薄い機能まで一つの抽象クラスへ集めるのは避けましょう。

たとえば、ユーザー情報、メール送信、ファイル保存、ログ出力、決済処理を一つの基底クラスにまとめると、責務が大きくなりすぎます。

抽象クラスは、同じ目的を持つ派生クラスに共通の基盤を提供するものです。クラス名を見たときに、何を表す基底クラスなのか説明できる状態が理想です。

8-6. 将来使うかもしれないという理由だけで抽象化しない

「将来、派生クラスが増えるかもしれない」という理由だけで、最初からすべてを抽象クラスにする必要はありません。

実際には派生クラスが一つしかなく、共通化する処理もない場合、抽象クラスを追加すると構造が複雑になるだけです。

まずは具体的な実装を作り、複数のクラスに共通する処理や明確な拡張ポイントが見つかった段階で抽象化する方法も有効です。

8-7. 継承よりコンポジションが適する場合もある

継承は、基底クラスと派生クラスの結び付きを強くします。基底クラスの変更が多くの派生クラスに影響する可能性があるため、常に継承が最適とは限りません。

ある機能を外部のクラスとして切り出し、そのオブジェクトをフィールドやコンストラクターで受け取る方法をコンポジションといいます。

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

public class OrderService
{
private readonly IMessageSender sender;

public OrderService(IMessageSender sender)
{
this.sender = sender;
}

public void CompleteOrder()
{
Console.WriteLine("注文を完了しました。");
sender.Send("注文が完了しました。");
}
}

OrderServiceは送信機能を継承するのではなく、IMessageSenderを利用しています。

「AはBの一種である」という関係なら継承が適している可能性があります。「AはBを利用する」という関係なら、コンポジションを検討しましょう。

9. C#のabstractに関するよくある質問

9-1. abstractクラスに通常のメソッドは定義できる?

抽象クラスには、処理本体を持つ通常のメソッドを定義できます。

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

public abstract void Speak();
}

Eatはすべての派生クラスで共通利用でき、Speakだけを派生クラスごとに実装できます。

抽象クラスの主な利点は、共通実装と抽象的な契約を同じクラスに定義できることです。

9-2. abstractクラスにコンストラクターは必要?

抽象クラスにコンストラクターは必須ではありません。

明示的に定義しなければ、条件を満たす場合は既定のコンストラクターが利用されます。ただし、派生クラスに共通する値を初期化したい場合は、コンストラクターを定義すると便利です。

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

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

外部から抽象クラスを直接生成することはないため、コンストラクターにはprotectedを指定するのが一般的です。

9-3. abstractクラスは複数継承できる?

C#では、複数の抽象クラスを同時に継承できません。

C#
// 複数のクラスは継承できない
// public class Sample : AbstractA, AbstractB

継承できる基底クラスは、抽象クラスか通常クラスかに関係なく一つだけです。

複数の役割を実装する必要がある場合は、一つの基底クラスと複数のインターフェースを組み合わせます。

C#
public class Sample :
BaseClass,
IReadable,
IWritable
{
}

9-4. abstractメソッドにprivateを指定できる?

抽象メソッドにprivateは指定できません。

抽象メソッドは派生クラスでオーバーライドする必要があります。しかし、privateメンバーは派生クラスからアクセスできないため、オーバーライドできません。

抽象メソッドには、一般的にpublicまたはprotectedを指定します。

C#
public abstract class Processor
{
protected abstract void ProcessCore();
}

外部には公開せず、派生クラスだけに実装させたい場合はprotected abstractが適しています。

9-5. abstractとstaticは同時に指定できる?

通常のクラスやクラス内のメソッドに、abstractstaticを同時に指定することはできません。

abstractは派生クラスのインスタンスメンバーとしてオーバーライドされることを前提とします。一方、staticメンバーはクラス自体に属し、通常のインスタンスメンバーのようにはオーバーライドされません。

C#
// コンパイルエラー
// public abstract static void Execute();

また、抽象クラスと静的クラスは役割が異なるため、クラスにabstract staticを同時指定することもできません。

なお、現在のC#では、インターフェースにstatic abstractメンバーを宣言できる機能があります。

C#
public interface IFactory<T>
{
static abstract T Create();
}

これはジェネリック型を通して静的メンバーを扱うための仕組みであり、抽象クラスの抽象メソッドとは用途が異なります。

9-6. 抽象クラスの中に抽象プロパティや抽象イベントを定義できる?

抽象クラスには、抽象プロパティや抽象イベントを定義できます。抽象インデクサーも定義可能です。

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

public abstract event EventHandler? Completed;
}

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

C#
public class DownloadTask : TaskBase
{
public override string Name => "ダウンロード";

public override event EventHandler? Completed;

public void Run()
{
Console.WriteLine("実行中です。");
Completed?.Invoke(this, EventArgs.Empty);
}
}

メソッドだけでなく、派生クラスに共通して必要なデータや通知の仕組みも抽象メンバーとして定義できます。

9-7. interfaceに実装を持てるならabstractクラスは不要?

インターフェースにデフォルト実装を記述できても、抽象クラスが不要になるわけではありません。

抽象クラスには、次のような特徴があります。

  • インスタンスフィールドを持てる

  • コンストラクターで共通の初期化ができる

  • protectedメンバーを通して派生クラス向けの機能を提供できる

  • 同じ種類のクラスに共通する状態と処理をまとめられる

インターフェースは、複数のクラスに共通の役割や契約を与える用途に適しています。

抽象クラスは「同じ種類のオブジェクトに共通する基盤」、インターフェースは「オブジェクトが持つ能力や契約」と考えると使い分けやすくなります。

9-8. abstractクラスを使う判断基準は?

抽象クラスを使うか迷ったときは、次の点を確認します。

  • 複数のクラスに明確な共通処理があるか

  • 派生クラスが同じ種類のオブジェクトといえるか

  • 派生クラス間で共通の状態を持つ必要があるか

  • 共通の初期化処理が必要か

  • 派生クラスに実装を強制したい処理があるか

  • 基底クラス型としてまとめて扱う必要があるか

これらに多く当てはまる場合は、抽象クラスが適している可能性があります。

一方、共通するのが操作の形だけである場合や、継承関係のないクラスに同じ役割を与えたい場合は、インターフェースが適しています。

共通処理を外部オブジェクトへ分離できる場合は、継承ではなくコンポジションを選ぶ方法もあります。

まとめ

C#のabstractは、継承を前提とした抽象クラスや、派生クラスでの実装を必要とする抽象メンバーを定義するためのキーワードです。

抽象クラスには、通常のメソッド、フィールド、プロパティ、コンストラクターを定義できます。共通する状態や処理を基底クラスにまとめながら、クラスごとに異なる処理を抽象メソッドとして派生クラスへ任せられます。

抽象メソッドには処理本体を記述せず、具体的な派生クラスでoverrideを使って実装します。これにより、実装漏れをコンパイル時に検出しながら、ポリモーフィズムを活用した柔軟な設計が可能です。

抽象クラスとインターフェースは似ていますが、主な役割が異なります。共通の状態や実装を持つ同種のクラスを表現する場合は抽象クラス、複数のクラスに共通の能力や契約を持たせる場合はインターフェースが適しています。

abstractを使えば必ず設計が良くなるわけではありません。継承関係が本当に自然か、共通化する責務が明確か、コンポジションのほうが適していないかを検討したうえで利用することが重要です。