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

はじめに

C#のabstractは、継承を前提としたクラス設計で使用するキーワードです。日本語では「抽象」と訳され、クラスの共通部分を基底クラスにまとめながら、具体的な処理を派生クラスに実装させるために使います。

abstractを理解すると、次のような設計ができるようになります。

  • 複数のクラスに共通する処理をまとめる

  • 派生クラスに必要なメソッドの実装を強制する

  • クラスごとに異なる処理を同じ方法で呼び出す

  • コードの重複を減らし、変更しやすい構造を作る

この記事では、C#の抽象クラスと抽象メソッドの基本から、継承、overridevirtual、インターフェースとの違い、よくあるエラーまで、初心者向けにサンプルコードを使って解説します。

1. C#のabstractとは

1-1. abstractキーワードの意味

C#のabstractは、クラスやメンバーが「未完成であり、派生クラスで具体化されること」を表すキーワードです。

クラスにabstractを付けると抽象クラスになります。

public abstract class Animal{}

メソッドにabstractを付けると抽象メソッドになります。

public abstract void Speak();

抽象メソッドには具体的な処理を書きません。抽象クラスを継承した派生クラスが、overrideを使って処理内容を実装します。

1-2. abstractを使う目的

abstractを使う主な目的は、複数の派生クラスに共通する設計上のルールを作ることです。

例えば、犬、猫、鳥を表すクラスには、すべて「鳴く」という動作があります。しかし、実際の鳴き方は動物ごとに異なります。

この場合、基底クラスで「すべての動物は鳴く」というルールだけを定義し、具体的な鳴き方を派生クラスに任せられます。

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

この設計により、Animalを継承する具象クラスは、原則としてSpeakメソッドを実装しなければなりません。

1-3. 抽象クラスと抽象メソッドの関係

抽象クラスは、抽象メソッドを定義できるクラスです。

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

抽象メソッドを持つクラスは、必ず抽象クラスとして宣言する必要があります。

一方、抽象クラスが必ず抽象メソッドを持つ必要はありません。インスタンス化だけを禁止し、共通処理を派生クラスに提供する目的でも抽象クラスを利用できます。

public abstract class ApplicationBase{public void Start(){Console.WriteLine("アプリケーションを開始します。");}}

1-4. abstractを使った設計が役立つ場面

abstractは、複数のクラスに明確な共通点がある場合に役立ちます。

代表的な例は次のとおりです。

  • 円や長方形などの図形クラス

  • 犬や猫などの動物クラス

  • 正社員やアルバイトなどの従業員クラス

  • クレジットカードや銀行振込などの決済クラス

  • CSVやJSONなどのファイル出力クラス

例えば、すべての決済方法には「支払う」という操作が必要ですが、その手順は決済方法ごとに異なります。このような場合に抽象メソッドを使うと、必要な操作を統一しながら個別の処理を実装できます。

2. C#の抽象クラスの基本

2-1. 抽象クラスの定義方法

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

public abstract class Animal{}

一般的な構文は次のとおりです。

アクセス修飾子 abstract class クラス名{// フィールド、プロパティ、メソッドなど}

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

2-2. 抽象クラスはインスタンス化できない

抽象クラスは未完成のクラスとして扱われるため、newを使って直接インスタンス化できません。

public abstract class Animal{}

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

抽象クラスを利用するには、抽象クラスを継承した具象クラスを作成します。

public class Dog : Animal{}

Dog dog = new Dog();

抽象クラス型の変数を宣言することは可能です。

Animal animal = new Dog();

この書き方では、実際に生成しているのはDogのインスタンスです。変数の型として抽象クラスを使うことで、複数の派生クラスを共通の型として扱えます。

2-3. 抽象クラスにフィールドやプロパティを定義する

抽象クラスには、通常のクラスと同じようにフィールドやプロパティを定義できます。

public abstract class Animal{private int _age;

public string Name { get; set; } = string.Empty;public int Age{get => _age;set{if (value < 0){throw new ArgumentOutOfRangeException(nameof(value));}_age = value;}}

}

共通の状態を抽象クラスに持たせることで、派生クラスごとに同じフィールドやプロパティを繰り返し定義する必要がなくなります。

2-4. 抽象クラスに通常のメソッドを定義する

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

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

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

}

派生クラスは、このメソッドをそのまま利用できます。

public class Dog : Animal{}

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

dog.Introduce();

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

私はポチです。

2-5. 抽象クラスにコンストラクターを定義する

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

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 = new Dog("ポチ");Console.WriteLine(dog.Name);

抽象クラスのコンストラクターは外部から直接呼び出すものではないため、通常はprotectedにします。

3. C#の抽象メソッドの基本

3-1. 抽象メソッドの定義方法

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

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

抽象メソッドの末尾には、メソッド本体を表す波括弧ではなくセミコロンを記述します。

public abstract void Speak();

次のように処理内容を記述することはできません。

public abstract void Speak(){Console.WriteLine("鳴きます");}

3-2. 抽象メソッドに処理内容を書けない理由

抽象メソッドは、派生クラスに「この操作を必ず用意してください」と要求するための定義です。

基底クラスでは、メソッド名、戻り値、引数などの形式だけを決めます。具体的な処理は派生クラスごとに異なるため、基底クラスにはメソッド本体を書きません。

既定の処理を基底クラスに用意し、必要に応じて派生クラスで変更したい場合は、abstractではなくvirtualを使います。

3-3. 抽象メソッドを派生クラスでoverrideする方法

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

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

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

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

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

ワンワン

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

3-4. 抽象メソッドの戻り値と引数の指定

抽象メソッドには、通常のメソッドと同じように戻り値や引数を指定できます。

public abstract class Calculator{public abstract decimal Calculate(decimal value1, decimal value2);}

派生クラスでは、戻り値の型、メソッド名、引数の型と並びを一致させます。

public class AdditionCalculator : Calculator{public override decimal Calculate(decimal value1, decimal value2){return value1 + value2;}}
Calculator calculator = new AdditionCalculator();decimal result = calculator.Calculate(10m, 20m);

Console.WriteLine(result);

実行結果は30です。

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

C#では、メソッドだけでなくプロパティも抽象メンバーとして定義できます。

public abstract class Product{public abstract decimal Price { get; }}

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

public class Book : Product{public override decimal Price { get; }

public Book(decimal price){Price = price;}

}

getsetの両方を持つ抽象プロパティも定義できます。

public abstract class Employee{public abstract string Name { get; set; }}
public class RegularEmployee : Employee{public override string Name { get; set; } = string.Empty;}

4. abstractを使った継承の基本

4-1. 基底クラスと派生クラスの関係

継承元となるクラスを基底クラス、継承して作られるクラスを派生クラスと呼びます。

public abstract class Animal{}

public class Dog : Animal{}

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

派生クラスは、アクセス可能な基底クラスのメンバーを引き継ぎます。これにより、共通のデータや処理を基底クラスにまとめられます。

4-2. 抽象クラスを継承する構文

抽象クラスを継承するには、派生クラス名の後ろにコロンと抽象クラス名を記述します。

public class Dog : Animal{}

C#では、クラスが直接継承できるクラスは1つだけです。そのため、複数の抽象クラスを同時に継承することはできません。

4-3. 派生クラスで抽象メンバーを実装する

抽象クラスを継承した具象クラスは、継承した抽象メンバーをすべて実装する必要があります。

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

public abstract void Move();

}

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

public override void Move(){Console.WriteLine("4本足で走ります。");}

}

一つでも実装しなかった場合、そのクラスを具象クラスとして宣言することはできません。実装を後続の派生クラスに任せる場合は、そのクラスもabstractにします。

4-4. baseキーワードで基底クラスの処理を呼び出す

baseキーワードを使うと、派生クラスから基底クラスのコンストラクターやメソッドを呼び出せます。

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

protected Employee(string name){Name = name;}public virtual void ShowInfo(){Console.WriteLine($"名前: {Name}");}

}

public class Manager : Employee{public string Department { get; }

public Manager(string name, string department): base(name){Department = department;}public override void ShowInfo(){base.ShowInfo();Console.WriteLine($"部署: {Department}");}

}

base(name)は基底クラスのコンストラクターを呼び出し、base.ShowInfo()は基底クラスのメソッドを実行します。

4-5. 抽象クラスを多段階で継承する

抽象クラスから別の抽象クラスを継承することも可能です。

public abstract class Vehicle{public abstract void Move();}

public abstract class Car : Vehicle{public abstract void Refuel();}

Carは抽象クラスなので、VehicleMoveを実装せず、さらに下位の派生クラスへ実装を任せられます。

public class GasolineCar : Car{public override void Move(){Console.WriteLine("エンジンで走行します。");}

public override void Refuel(){Console.WriteLine("ガソリンを給油します。");}

}

多段階の継承は可能ですが、階層が深くなると処理の定義場所を追いにくくなるため、必要以上に複雑にしないことが重要です。

4-6. 抽象クラスを使ったポリモーフィズム

ポリモーフィズムとは、共通の型を通じて、実際のインスタンスに応じた異なる処理を実行できる仕組みです。

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 animal1 = new Dog();Animal animal2 = new Cat();

animal1.Speak();animal2.Speak();

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

ワンワンニャー

変数の型はどちらもAnimalですが、実際のインスタンスに応じて適切なSpeakメソッドが呼び出されます。

5. abstractの実践的なサンプルコード

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

円や長方形は形が異なりますが、どちらも面積を計算できます。

public abstract class Shape{public abstract double CalculateArea();

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

}

円クラスを定義します。

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

public Circle(double radius){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){Width = width;Height = height;}public override double CalculateArea(){return Width * Height;}

}

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

Shape circle = new Circle(3);Shape rectangle = new Rectangle(4, 5);

circle.ShowArea();rectangle.ShowArea();

ShowAreaは基底クラスに共通化し、計算方法だけを派生クラスで実装しています。

5-2. 動物クラスで鳴き方を派生クラスに実装させる例

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

protected Animal(string name){Name = name;}public abstract void Speak();

}

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

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

}

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

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

}

Animal dog = new Dog("ポチ");Animal cat = new Cat("ミケ");

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

基底クラスは名前という共通の状態を管理し、鳴き方だけを派生クラスに任せています。

5-3. 共通処理と個別処理を組み合わせる例

共通処理の途中に、派生クラス固有の処理を組み込む設計もできます。

public abstract class DataExporter{public void Export(){Console.WriteLine("出力処理を開始します。");Validate();WriteData();Console.WriteLine("出力処理が完了しました。");}

protected virtual void Validate(){Console.WriteLine("共通の入力チェックを行います。");}protected abstract void WriteData();

}

public class CsvExporter : DataExporter{protected override void WriteData(){Console.WriteLine("CSV形式でデータを書き込みます。");}}
DataExporter exporter = new CsvExporter();exporter.Export();

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

5-4. 抽象プロパティを使う例

public abstract class TaxCalculator{public abstract decimal TaxRate { get; }

public decimal CalculateTax(decimal price){return price * TaxRate;}

}

public class StandardTaxCalculator : TaxCalculator{public override decimal TaxRate => 0.10m;}

public class ReducedTaxCalculator : TaxCalculator{public override decimal TaxRate => 0.08m;}

TaxCalculator standard = new StandardTaxCalculator();TaxCalculator reduced = new ReducedTaxCalculator();

Console.WriteLine(standard.CalculateTax(1000m));Console.WriteLine(reduced.CalculateTax(1000m));

税額計算の方法を共通化し、税率だけを派生クラスごとに変更しています。

5-5. 抽象クラスのコレクションをループ処理する例

抽象クラス型のコレクションには、異なる派生クラスのインスタンスを格納できます。

List<Shape> shapes = new(){new Circle(3),new Rectangle(4, 5),new Circle(2)};

foreach (Shape shape in shapes){Console.WriteLine(shape.CalculateArea());}

呼び出し側は、各要素が円か長方形かを判定する必要がありません。すべてをShapeとして扱い、CalculateAreaを呼び出すだけで適切な処理が実行されます。

6. abstractとvirtualの違い

6-1. abstractは派生クラスでの実装が必須

abstractメソッドは処理内容を持たず、具象派生クラスでの実装が必須です。

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

基底クラス側で共通の既定処理を決められない場合に適しています。

6-2. virtualは基底クラスに既定の処理を持てる

virtualメソッドは、基底クラスに処理内容を定義できます。

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

派生クラスは、必要な場合だけoverrideします。

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

6-3. abstractとvirtualの使い分け

基底クラスに適切な既定処理が存在しない場合はabstractを使います。

public abstract double CalculateArea();

共通の既定処理があり、一部の派生クラスだけ変更すればよい場合はvirtualを使います。

public virtual void Save(){Console.WriteLine("標準形式で保存します。");}

判断基準は「派生クラスが必ず独自の処理を用意すべきか」です。

6-4. overrideしなかった場合の違い

abstractメソッドを具象派生クラスでoverrideしなかった場合は、コンパイルエラーになります。

一方、virtualメソッドをoverrideしなかった場合は、基底クラスに定義された既定の処理が実行されます。

public class Cat : Animal{// virtualメソッドならoverrideしなくてもよい}

6-5. abstractとvirtualを併用する設計例

抽象クラスでは、abstractvirtualを組み合わせられます。

public abstract class Report{public abstract string CreateBody();

public virtual string CreateTitle(){return "標準レポート";}public void Print(){Console.WriteLine(CreateTitle());Console.WriteLine(CreateBody());}

}

public class SalesReport : Report{public override string CreateBody(){return "売上データを表示します。";}

public override string CreateTitle(){return "月次売上レポート";}

}

本文の作成は必須とし、タイトルには変更可能な既定値を用意しています。

7. 抽象クラスとインターフェースの違い

7-1. 継承できる数の違い

C#のクラスが直接継承できるクラスは1つだけです。そのため、継承できる抽象クラスも1つです。

public class Dog : Animal{}

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

public class Dog : Animal, IRunnable, ISwimmable{}

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

7-2. フィールドやコンストラクターを持てるか

抽象クラスは、インスタンスフィールドやコンストラクターを持てます。

public abstract class Animal{private readonly string _name;

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

}

インターフェースは、クラスのインスタンス状態を保持するための通常のインスタンスフィールドや、インスタンスコンストラクターを持てません。

そのため、複数の派生クラスで共通の状態を管理したい場合は、抽象クラスが適しています。

7-3. 共通処理を実装できるか

抽象クラスには通常のメソッドを定義できるため、共通処理を実装できます。

public abstract class Animal{public void Sleep(){Console.WriteLine("眠ります。");}}

現在のC#では、インターフェースにも既定の実装を持つメンバーを定義できます。ただし、状態の共有やコンストラクターを含むクラス階層の基盤を作る用途では、抽象クラスの方が自然です。

7-4. 状態を共有できるか

抽象クラスは、フィールドやプロパティを通じて状態を共有できます。

public abstract class Employee{public string Name { get; }public DateTime JoinedAt { get; }

protected Employee(string name, DateTime joinedAt){Name = name;JoinedAt = joinedAt;}

}

インターフェースは、基本的にクラスが提供すべき機能や契約を表すために使います。各インスタンスの状態を共通フィールドとして保持する用途には向きません。

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

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

  • 派生クラス間に明確な「同じ種類」という関係がある

  • 共通のフィールドやプロパティを持たせたい

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

  • 共通処理と派生クラス固有の処理を組み合わせたい

  • 継承階層全体を一体として管理したい

例えば、DogCatはどちらもAnimalの一種であるため、抽象クラスによる継承と相性がよい設計です。

7-6. インターフェースを選ぶべきケース

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

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

  • 複数の契約を一つのクラスに実装させたい

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

  • クラスの内部状態ではなく、公開する操作を定義したい

  • 疎結合な設計を重視したい

例えば、人、自動車、ロボットに「移動できる」という共通機能を持たせる場合、IMovableのようなインターフェースが適しています。

7-7. 抽象クラスとインターフェースを併用する方法

抽象クラスがインターフェースを実装することもできます。

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

public abstract class ServiceBase : ILogger{public void Execute(){Log("処理を開始します。");ExecuteCore();Log("処理を終了します。");}

public virtual void Log(string message){Console.WriteLine(message);}protected abstract void ExecuteCore();

}

public class OrderService : ServiceBase{protected override void ExecuteCore(){Console.WriteLine("注文処理を実行します。");}}

インターフェースで外部への契約を定義し、抽象クラスで共通実装を提供する設計です。

8. abstractと関連キーワードの違い

8-1. abstractとoverrideの関係

abstractは、基底クラスで実装が必要なメンバーを宣言するためのキーワードです。

overrideは、基底クラスの抽象メンバーまたは仮想メンバーを派生クラスで実装・再定義するためのキーワードです。

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

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

abstractoverrideは対立するものではなく、基底クラスと派生クラスで対応する関係にあります。

8-2. abstractとsealedの違い

abstractクラスは継承されることを前提とします。一方、sealedクラスは継承を禁止します。

public abstract class Animal{}

public sealed class Dog : Animal{}

この例では、Dogからさらに別のクラスを派生させることはできません。

同じクラスを単純にabstract sealed classとして宣言することはできません。ただし、抽象メンバーを派生クラスで実装し、それ以上のオーバーライドを禁止するsealed overrideは使用できます。

public abstract class BaseClass{public abstract void Execute();}

public class DerivedClass : BaseClass{public sealed override void Execute(){Console.WriteLine("処理を実行します。");}}

8-3. abstractとnewの違い

newキーワードをメンバーの宣言に付けると、基底クラスの同名メンバーを隠蔽します。

public class BaseClass{public void Show(){Console.WriteLine("基底クラス");}}

public class DerivedClass : BaseClass{public new void Show(){Console.WriteLine("派生クラス");}}

overrideによるオーバーライドでは、実際のインスタンスに応じて呼び出される処理が決まります。一方、newによる隠蔽では、変数の宣言型によって呼び出されるメソッドが変わります。

ポリモーフィズムを目的とする場合は、通常abstractvirtualoverrideを使います。

8-4. abstract classとstatic classの違い

抽象クラスは直接インスタンス化できませんが、派生クラスを作成できます。

public abstract class Animal{}

静的クラスもインスタンス化できませんが、継承することもできません。

public static class MathUtility{public static int Add(int x, int y){return x + y;}}

抽象クラスは継承を使って共通設計を作るためのものです。静的クラスは、インスタンスの状態を必要としない関連機能をまとめるために使います。

8-5. abstractメンバーに指定できるアクセス修飾子

抽象メンバーは、派生クラスからアクセスして実装できる必要があります。そのため、privateにはできません。

一般的にはpublicまたはprotectedを使用します。

public abstract class DataProcessor{public abstract void Execute();

protected abstract void Validate();

}

同じアセンブリ内に公開するinternalや、条件を組み合わせたprotected internalprivate protectedも設計に応じて利用できます。

ただし、抽象メンバーの公開範囲を必要以上に広げると、外部から依存される範囲も広がります。外部へ公開する必要がない処理は、protectedなどを検討します。

9. abstractを使うメリット

9-1. 派生クラスに必要な実装を強制できる

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

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

新しい決済クラスを追加した際にPayの実装を忘れると、コンパイラーがエラーとして検出します。

設計上必要な機能の実装漏れを、実行前に発見できる点がメリットです。

9-2. 共通処理を基底クラスに集約できる

複数のクラスで同じ処理を使う場合、抽象クラスにまとめられます。

public abstract class Payment{protected void ValidateAmount(decimal amount){if (amount <= 0){throw new ArgumentOutOfRangeException(nameof(amount));}}

public abstract void Pay(decimal amount);

}

入力確認やログ出力などを基底クラスに集約すると、派生クラスは固有の処理に集中できます。

9-3. コードの重複を減らせる

同じ処理を複数のクラスにコピーすると、仕様変更のたびにすべてのクラスを修正しなければなりません。

共通処理を抽象クラスにまとめれば、修正箇所を減らせます。その結果、修正漏れやクラス間の動作の不一致を防ぎやすくなります。

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

派生クラスを抽象クラス型として扱うことで、呼び出し側を具体的なクラスから切り離せます。

public void ProcessPayment(Payment payment, decimal amount){payment.Pay(amount);}

ProcessPaymentは、クレジットカード決済か銀行振込かを知る必要がありません。Paymentを継承しているクラスであれば、同じ方法で処理できます。

9-5. 機能追加や仕様変更に対応しやすくなる

抽象クラスを基盤として設計すると、新しい派生クラスを追加しても既存の呼び出し側を変更せずに済む場合があります。

List<Payment> payments = new(){new CreditCardPayment(),new BankTransferPayment()};

新しい決済方法を追加するときも、Paymentを継承して必要なメソッドを実装すれば、既存の処理へ組み込みやすくなります。

10. abstractを使う際の注意点とよくあるエラー

10-1. 抽象クラスをnewして発生するエラー

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

public abstract class Animal{}

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

派生クラスを生成し、抽象クラス型の変数へ代入します。

Animal animal = new Dog();

10-2. 抽象メソッドを実装せずに発生するエラー

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

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

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

Speakを実装するか、Dogも抽象クラスとして宣言します。

public abstract class Dog : Animal{}

10-3. overrideキーワードを付け忘れる

抽象メソッドを実装するときは、overrideが必要です。

public class Dog : Animal{// overrideがないため正しい実装にならないpublic void Speak(){Console.WriteLine("ワンワン");}}

正しくは次のように書きます。

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

10-4. メソッドの引数や戻り値が一致していない

オーバーライドするメソッドは、基底クラスの定義とシグネチャを一致させる必要があります。

public abstract class Calculator{public abstract decimal Calculate(decimal value);}

次の実装は、引数や戻り値が異なるため正しいオーバーライドではありません。

public int Calculate(int value){return value;}

正しくは次のように実装します。

public override decimal Calculate(decimal value){return value;}

10-5. privateな抽象メンバーを定義できない

privateメンバーは派生クラスからアクセスできません。そのため、派生クラスでの実装を前提とする抽象メンバーにprivateは指定できません。

public abstract class Sample{// コンパイルエラーprivate abstract void Execute();}

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

public abstract class Sample{protected abstract void Execute();}

10-6. staticメンバーとabstractの扱いに注意する

抽象クラスの通常の抽象メソッドはインスタンスに対する操作であり、static abstractとして宣言することはできません。

public abstract class Sample{// 抽象クラスでは使用できないpublic static abstract void Execute();}

抽象クラス内に通常の静的メンバーを定義すること自体は可能です。

public abstract class Sample{public static void ShowMessage(){Console.WriteLine("共通メッセージ");}

public abstract void Execute();

}

なお、比較的新しいC#では、インターフェースにstatic abstractメンバーを定義できる仕組みがあります。これはジェネリックな数値処理などで使われる機能であり、抽象クラスのインスタンスメンバーとは異なります。

10-7. 継承階層を深くしすぎない

抽象クラスは便利ですが、継承階層を深くしすぎると次の問題が起こります。

  • メソッドの定義場所を探しにくい

  • 一つの変更が多くの派生クラスへ影響する

  • 基底クラスへの依存が強くなる

  • テストや機能の差し替えが難しくなる

  • クラスの役割を理解しにくくなる

継承は「AはBの一種である」という関係が明確な場合に使い、単に処理を再利用したいだけなら、別クラスへ処理を分離して組み合わせるコンポジションも検討します。

11. abstractを使うべきか判断するポイント

11-1. 複数のクラスに共通する基盤があるか

抽象クラスを作る前に、派生クラス同士に本当に共通の基盤があるかを確認します。

例えば、円と長方形には「図形であり、面積を計算できる」という共通点があります。この場合、Shape抽象クラスを作ることには合理性があります。

一方、偶然同じ名前のメソッドを持っているだけでは、同じ基底クラスへまとめる理由として不十分です。

11-2. 派生クラスごとに異なる処理が必要か

共通する操作はあるものの、具体的な処理が派生クラスごとに異なる場合は、抽象メソッドが適しています。

public abstract decimal CalculateFee();

すべての派生クラスで同じ処理を使えるなら、通常のメソッドとして基底クラスに実装します。共通の既定処理を変更可能にしたい場合はvirtualを検討します。

11-3. 実装を強制する必要があるか

派生クラスに必ず実装してほしい処理がある場合は、abstractが有効です。

例えば、すべての通知クラスにSendメソッドが必要なら、次のように宣言できます。

public abstract class Notification{public abstract void Send(string message);}

必須ではなく、共通の既定動作で十分ならvirtualの方が適しています。

11-4. 継承よりコンポジションが適していないか

コンポジションとは、必要な機能を別のクラスとして持たせ、組み合わせて利用する設計です。

例えば、ログ出力機能を継承で提供する代わりに、ロガーをフィールドやコンストラクター引数として受け取れます。

public class OrderService{private readonly ILogger _logger;

public OrderService(ILogger logger){_logger = logger;}

}

コンポジションには、機能を差し替えやすく、クラス同士の結び付きを弱くできるメリットがあります。

継承を選ぶ前に、次の関係を確認します。

  • 継承:AはBの一種である

  • コンポジション:AはBを持っている、または利用する

11-5. 将来の拡張を考慮した設計になっているか

将来追加される派生クラスを予測しすぎて、最初から複雑な抽象クラスを作ると、かえって変更しにくくなることがあります。

抽象化は、実際に複数の実装が存在し、共通点と相違点が明確になってから行うことも重要です。

基底クラスには安定した共通部分だけを置き、派生クラス固有の処理を詰め込みすぎないようにします。

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

12-1. 抽象クラスに抽象メソッドがなくてもよい?

抽象クラスに抽象メソッドがなくても問題ありません。

public abstract class ApplicationBase{public void Start(){Console.WriteLine("開始します。");}}

この設計は、基底クラスを直接インスタンス化させず、派生クラスを通じてのみ利用させたい場合に使えます。

12-2. 抽象クラスから抽象クラスを継承できる?

抽象クラスから別の抽象クラスを継承できます。

public abstract class Vehicle{public abstract void Move();}

public abstract class Car : Vehicle{public abstract void OpenTrunk();}

派生した抽象クラスは、基底クラスの抽象メンバーを実装しても、さらに下位の派生クラスへ実装を任せても構いません。

12-3. 抽象クラスに複数のコンストラクターを定義できる?

抽象クラスにも、引数の異なる複数のコンストラクターを定義できます。

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

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

}

派生クラスは、用途に応じて呼び出す基底クラスのコンストラクターを選択できます。

12-4. 抽象メソッドを一部だけ実装できる?

派生クラスも抽象クラスとして宣言するなら、一部だけ実装できます。

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

public abstract class Mammal : Animal{public override void Move(){Console.WriteLine("地面を移動します。");}}

MammalではMoveだけを実装し、Speakはさらに下位の派生クラスへ任せています。

具象クラスとして宣言する場合は、継承した未実装の抽象メンバーをすべて実装しなければなりません。

12-5. 抽象クラスはインターフェースを実装できる?

抽象クラスはインターフェースを実装できます。

public interface IPrintable{void Print();}

public abstract class Document : IPrintable{public abstract void Print();}

抽象クラスでインターフェースのメンバーを実装せず、抽象メンバーとして宣言し直して派生クラスへ実装を任せることも可能です。

12-6. 抽象クラスのインスタンスを変数として扱える?

抽象クラスそのもののインスタンスは生成できませんが、抽象クラス型の変数は宣言できます。

Animal animal = new Dog();

また、抽象クラス型の配列やコレクションも作成できます。

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

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

12-7. abstractはどのような規模の開発で役立つ?

abstractは大規模開発だけの機能ではありません。小規模なプログラムでも、複数のクラスに共通部分があり、個別実装を明確に分けたい場合に役立ちます。

ただし、派生クラスが一つしかなく、将来増える根拠もない場合は、抽象クラスを作ることで構造が不必要に複雑になる可能性があります。

プロジェクトの規模ではなく、次の条件を基準に判断することが重要です。

  • 複数のクラスに安定した共通部分がある

  • 派生クラスごとに異なる実装が必要

  • 必要なメンバーの実装を強制したい

  • 共通の状態や処理を共有したい

  • 共通の型としてまとめて扱いたい

まとめ

C#のabstractは、共通部分を基底クラスにまとめ、具体的な処理を派生クラスに実装させるためのキーワードです。

抽象クラスは直接インスタンス化できませんが、フィールド、プロパティ、コンストラクター、通常のメソッドを持てます。抽象メソッドは処理内容を持たず、具象派生クラスがoverrideを使って実装します。

abstractを活用すると、派生クラスに必要な実装を強制しながら、共通処理の集約、コード重複の削減、ポリモーフィズムの活用が可能になります。

一方で、すべての共通処理を抽象クラスへ集めると、継承階層が複雑になり、クラス間の依存も強くなります。既定処理を変更可能にしたい場合はvirtual、複数のクラスへ共通の契約を持たせたい場合はインターフェース、機能を柔軟に組み合わせたい場合はコンポジションも検討しましょう。

「共通する基盤があるか」「派生クラスごとに異なる処理が必要か」「実装を強制すべきか」を確認することが、C#のabstractを適切に使うための重要なポイントです。