抽象クラスは、派生されるためだけに存在するクラスです。インスタンス化できず、抽象メンバー、つまりすべての具象の派生クラスが実装しなければならない本体のないメソッドやプロパティを宣言できます。それ以外の点では普通のクラスで、フィールド、コンストラクター、そして派生クラスが継承する完全に実装されたメソッドを持てます。
抽象クラスの宣言
クラスに abstract を付け、派生クラスが用意しなければならない各メンバーに abstract を付けて、本体の代わりにセミコロンで終えます。
出力:
Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00
Describe は基底クラスに一度だけ書かれ、Shape にはまったく存在しない Area と Perimeter を呼んでいます。これが抽象メンバーの要点です。基底クラスはそれに頼ることができ、コンパイラはすべての具象の図形がそれを用意することを保証します。1つでも忘れた派生クラスはコンパイルできません。
class Triangle : Shape
{
public Triangle() : base("Triangle") { }
public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'
直し方は Perimeter を実装することです。Triangle がさらに別のクラスのための中間の基底なら、Triangle 自体に abstract を付けます。
抽象メンバーのルール
- 抽象メンバーは本体を持たず、暗黙的に仮想です。派生クラスは
overrideで実装します。 - 抽象メンバーを含められるのは抽象クラスだけです。普通のクラスに置くとコンパイルエラーになります。
- クラスの抽象メンバーは
private(誰もオーバーライドできない)やstaticにはできません。 - メソッド、プロパティ、インデクサー、イベントは抽象にできます。フィールドとコンストラクターはできません。
- 抽象クラスが抽象メンバーを含む必要はありません。クラスに
abstractを付けるだけでインスタンス化を防げるので、サブクラスを通してしか意味をなさない基底クラスに便利です。
抽象プロパティは、派生クラスが用意しなければならないアクセサーを宣言します。
abstract class Plan
{
public abstract decimal MonthlyPrice { get; }
public abstract string Name { get; }
}
class ProPlan : Plan
{
public override decimal MonthlyPrice => 12.99m;
public override string Name => "Pro";
}
抽象クラスのコンストラクター
抽象クラスはコンストラクターを持てます。new では呼べませんが、すべての派生クラスのコンストラクターが : base(...) を通してどれかを実行するので、共通の状態を初期化して検証する場所になります。protected として宣言します。これは「派生クラスだけがこれを使える」ということをそのまま表しています。
テンプレートメソッドパターン
抽象クラスを書く最も一般的な理由は、アルゴリズムの順序を基底クラスで固定し、個々のステップを派生クラスに埋めさせることです。ステップを実行する基底のメソッドは仮想ではないので、どのサブクラスもステップを並べ替えたり飛ばしたりできません。
出力:
item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20
| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |
Total: 26.20
各行は名前付きのタプルで、出力はStringBuilderで組み立てています。組み合わせ方に注目してください。Header と Row は、どの形式でも決める必要があるので abstract で、Footer は、ほとんどの形式では何も必要ないので空の既定を持つ virtual です。ステップは protected なので、外部のコードが呼べるのは Export だけです。フレームワークのクラスも同じ考え方を使っています。Stream と TextWriter は抽象基底クラスで、多くの便利なメソッドが、派生クラスが用意する少数のメンバー(ストリームなら Read と Write、テキストライターなら Write(char))の上に作られています。
抽象クラスとインターフェース
どちらを使っても、具象クラスを知らずに型に対してコードを書けます。違いは、何を含められるかと、クラスがいくつ持てるかです。
| 抽象クラス | インターフェース | |
|---|---|---|
| クラスが継承または実装できる数 | 1つ | 複数 |
| インスタンスフィールド(状態) | 可 | 不可 |
| コンストラクター | 可 | 不可 |
| 本体を持つメソッド | 可 | 可、C# 8以降(既定のインターフェースメソッド) |
| 静的メンバー | 可 | 可、C# 8以降。静的抽象メンバーはC# 11以降 |
| メンバーのアクセス修飾子 | 任意 | 既定でpublic。C# 8以降は他も可 |
| 構造体が使えるか | 不可 | 可 |
| 後からメンバーを追加する | 抽象でなければ安全 | 既定の本体がない限り実装側が壊れる |
既定のインターフェースメソッド(C# 8)で差は縮まりましたが、中心的な違いは残っています。
public interface ILogger
{
void Write(string message);
void Error(string message) => Write("ERROR: " + message); // default implementation: C# 8, .NET Core 3.0+
}
既定のメソッドも、インターフェースにはインスタンスフィールドがないので、それに触れることはできません。呼べるのはインターフェースの他のメンバーだけです。
実際的な選び方:
- インターフェースは、無関係な型が持ちうる能力(
IComparable<T>、IDisposable、IShippingProvider)を表す場合、構造体も参加できるようにしたい場合、クラスがそうした役割をいくつも必要とする場合に使います。ほとんどの依存性の注入のコードがインターフェースに対して書かれるのはこのためです。 - 抽象クラスは、派生型が本当に1つのものの変種で、上のエクスポーターのように状態や固定のアルゴリズムを共有する場合に使います。
- 両方使うのもよくあることです。呼び出し側が依存するインターフェースと、それを実装して、実装する側が共通部分を書き直さずに済むようにする抽象基底クラスです。インターフェースの側についてはインターフェースのページで詳しく扱います。
よくある間違い
- 基底クラスをインスタンス化しようとする。
new Shape(...)はCS0144です。派生クラスをインスタンス化します。 - 実装に
overrideを付け忘れる。 派生クラスでpublic double Area()と書いても抽象メンバーの実装にはなりません。コンパイラは実装がないことをCS0534で報告し、さらに新しいメソッドが継承したものを隠すという警告も出します。 - 抽象クラスの公開のコンストラクター。 害はありませんが誤解を招きます。
protectedを使います。 - 共通のコードのない抽象クラス。 基底クラスに抽象メンバーしかなく状態もないなら、インターフェースで同じことを表せ、1つしかない基底クラスの枠も使わずに済みます。
- 基底のコンストラクターから抽象メンバーを呼ぶ。 派生クラスの実装は派生クラスのコンストラクターの本体より前に実行されるので、その本体が代入するフィールドはまだ
0やnullです。
よくある質問
C#の抽象クラスとは何ですか?
abstract を付けたクラスは基底クラスとしてだけ使うためのもので、new でインスタンスを作ることはできません(エラーCS0144)。通常のフィールド、コンストラクター、メソッドに加えて、本体を持たず、抽象でないすべての派生クラスが実装しなければならない abstract メンバーを含められます。
C#の抽象クラスとインターフェースの違いは何ですか?
クラスが派生できる抽象クラスは1つだけですが、インターフェースはいくつでも実装できます。抽象クラスは状態(インスタンスフィールド)、コンストラクター、任意のアクセスレベルのメンバーを持て、共通の実装を置くのに自然な場所です。インターフェースはインスタンスの状態を持たない能力を表します。C# 8以降は既定のメソッドの本体を含められますが、インスタンスフィールドやコンストラクターは今も持てません。
C#の抽象メソッドと仮想メソッドの違いは何ですか?
virtual メソッドは本体を持ち、派生クラスがオーバーライドしてもかまいません。abstract メソッドは本体を持たず、派生クラスはそれをオーバーライドしなければならず、しなければその派生クラス自体が抽象になります(具象クラスならエラーCS0534)。抽象メソッドは抽象クラスの中にしか書けません。
C#の抽象クラスはコンストラクターを持てますか?
持てます。派生クラスが構築されるときに : base(...) を通して実行され、通常は共通のフィールドを初期化します。protected として宣言します。抽象クラスの公開のコンストラクターは、呼び出し側が使えるかのように見せますが、実際には使えません。
抽象クラスのインスタンスを作れないのはなぜですか?
実装のない抽象メンバーを持っているかもしれないからです。new Shape().Area() を呼んでも、実行すべきコードがありません。コンパイラはCS0144(「Cannot create an instance of the abstract type or interface」)を報告します。代わりに具象の派生クラスのインスタンスを作り、必要なら抽象型の変数に保持します:Shape s = new Circle(2);。