インターフェースと抽象クラスは C# の設計において「何ができるかを定義する」仕組みです。「どちらを使えばいいのか」は初学者が最も迷うポイントの一つであり、C# 8 以降でインターフェースにデフォルト実装が追加されてからますます判断が難しくなっています。
本記事では基本の違いから、多重実装・デフォルト実装・明示的インターフェース実装・テンプレートメソッドパターン・設計の選択基準まで体系的に解説します。
インターフェースの基本
インターフェースは「このクラスはこのメソッドを必ず持つ」という契約(contract)を定義します。実装の中身はなく、シグネチャ(メソッド名・引数・戻り値)だけを宣言します。クラスはインターフェースを「実装(implements)」し、すべてのメンバーを実装する義務を負います。
// インターフェース: 命名規則は先頭に "I" を付ける
interface ILogger
{
void Log(string message);
void LogError(string message, Exception? ex = null);
}
// 実装クラス① : コンソール出力
class ConsoleLogger : ILogger
{
public void Log(string message)
=> Console.WriteLine($"[INFO] {message}");
public void LogError(string message, Exception? ex = null)
=> Console.WriteLine($"[ERROR] {message}{(ex is null ? "" : $": {ex.Message}")}");
}
// 実装クラス② : ファイル出力(同じインターフェースを別実装)
class FileLogger : ILogger
{
private readonly string _path;
public FileLogger(string path) => _path = path;
public void Log(string message)
=> File.AppendAllText(_path, $"[INFO] {message}
");
public void LogError(string message, Exception? ex = null)
=> File.AppendAllText(_path, $"[ERROR] {message}
");
}
// 利用側はインターフェース型で受け取る → 実装に依存しない
void RunApp(ILogger logger)
{
logger.Log("アプリ起動");
logger.Log("処理完了");
}
RunApp(new ConsoleLogger());
RunApp(new FileLogger("app.log"));
複数インターフェースの実装(多重能力の付与)
C# のクラスは1つのクラスしか継承できませんが、インターフェースは何個でも実装できます。これにより「ログできる」「保存できる」「通知できる」という複数の能力を自由に組み合わせて付与できます。
interface IReadable { string Read(); }
interface IWritable { void Write(string data); }
interface ICloseable { void Close(); }
// 3つのインターフェースを同時に実装
class FileStream2 : IReadable, IWritable, ICloseable
{
private readonly string _path;
public FileStream2(string path) => _path = path;
public string Read() => File.ReadAllText(_path);
public void Write(string d) => File.WriteAllText(_path, d);
public void Close() => Console.WriteLine("ファイルを閉じました");
}
// 必要な能力だけを受け取るメソッドが書ける
void SaveData(IWritable w, string data) => w.Write(data);
void Cleanup(ICloseable c) => c.Close();
var fs = new FileStream2("data.txt");
SaveData(fs, "Hello, World!");
Cleanup(fs);
インターフェースの新機能(C# 8以降)
デフォルト実装(Default Interface Methods)
C# 8 から、インターフェースのメソッドにデフォルト実装を書けるようになりました。実装クラスがそのメソッドをオーバーライドしなければ、デフォルト実装が使われます。主に既存のインターフェースに後から機能を追加するときの互換性維持に使います。
interface IShape
{
double Area();
double Perimeter();
// デフォルト実装: 実装クラスがオーバーライドしなくてもコンパイルできる
string Describe()
=> $"面積: {Area():F2}、周囲: {Perimeter():F2}";
}
class Circle2 : IShape
{
public double Radius { get; }
public Circle2(double r) => Radius = r;
public double Area() => Math.PI * Radius * Radius;
public double Perimeter() => 2 * Math.PI * Radius;
// Describe() は実装しない → デフォルト実装が使われる
}
class Square3 : IShape
{
public double Side { get; }
public Square3(double s) => Side = s;
public double Area() => Side * Side;
public double Perimeter() => 4 * Side;
// オーバーライドして独自の説明にする
public string Describe() => $"正方形(一辺: {Side})";
}
IShape c = new Circle2(5);
IShape s = new Square3(4);
Console.WriteLine(c.Describe()); // 面積: 78.54、周囲: 31.42(デフォルト)
Console.WriteLine(s.Describe()); // 正方形(一辺: 4)(オーバーライド)
Circle2 c = new Circle2(5);)からは Describe() を直接呼び出せないためご注意ください(オーバーライドしていない場合)。これは抽象クラスとの重要な違いです。静的メンバーとインターフェースの static abstract(C# 11)
C# 11 から static abstract メンバーをインターフェースで宣言できます。ジェネリクスと組み合わせて、型パラメーター自体の静的メソッドを制約できます。
// 数値型の加算を抽象化する例
interface IAddable<T> where T : IAddable<T>
{
static abstract T operator +(T a, T b);
static abstract T Zero { get; }
}
struct Meters : IAddable<Meters>
{
public double Value { get; }
public Meters(double v) => Value = v;
public static Meters operator +(Meters a, Meters b) => new(a.Value + b.Value);
public static Meters Zero => new(0);
public override string ToString() => $"{Value}m";
}
// ジェネリクスで任意の IAddable<T> 型の合計を計算できる
static T Sum<T>(IEnumerable<T> items) where T : IAddable<T>
=> items.Aggregate(T.Zero, (acc, x) => acc + x);
var distances = new[] { new Meters(1.5), new Meters(2.3), new Meters(0.8) };
Console.WriteLine(Sum(distances)); // 4.6m
明示的インターフェース実装
1つのクラスが同名のメソッドを持つ複数のインターフェースを実装するとき、明示的インターフェース実装で衝突を回避できます。メソッド名の前にインターフェース名を付けて定義し、その実装はインターフェース型の変数からのみアクセスできます。
interface IPrintable { void Print(); }
interface IDisplayable { void Print(); } // 同名メソッド
class Document : IPrintable, IDisplayable
{
// 通常の実装(クラス型からも呼べる)
public void Print()
=> Console.WriteLine("[Document] 通常の Print()");
// 明示的実装(IPrintable 型からのみ呼べる)
void IPrintable.Print()
=> Console.WriteLine("[IPrintable] 印刷用の Print()");
// 明示的実装(IDisplayable 型からのみ呼べる)
void IDisplayable.Print()
=> Console.WriteLine("[IDisplayable] 表示用の Print()");
}
var doc = new Document();
doc.Print(); // [Document] 通常の Print()
IPrintable printable = doc;
IDisplayable displayable = doc;
printable.Print(); // [IPrintable] 印刷用の Print()
displayable.Print(); // [IDisplayable] 表示用の Print()
// IDisposable の Dispose() を明示的に実装して
// 「使う人が直接 Dispose() を呼ぶ」より「using で使う」設計を促す例
class ResourceHolder : IDisposable
{
private bool _disposed;
// 明示的実装: 直接 .Dispose() は呼ばせず using を促す
void IDisposable.Dispose()
{
if (!_disposed)
{
Console.WriteLine("リソースを解放しました");
_disposed = true;
}
}
public void DoWork()
=> Console.WriteLine("処理中…");
}
using (IDisposable r = new ResourceHolder())
{
((ResourceHolder)r).DoWork(); // using で自動解放
} // Dispose() が呼ばれる
抽象クラスの基本
抽象クラスは「共通処理をまとめた基盤クラス」と「子クラスに強制する未実装部分」を組み合わせたものです。abstract キーワードを付けたクラスは直接 new できません。
// 抽象クラス: 直接 new はできない
abstract class DataExporter
{
// フィールド・コンストラクタを持てる(インターフェースにはない)
protected string _outputPath;
protected DataExporter(string outputPath)
{
_outputPath = outputPath;
}
// 通常メソッド(共通処理。子クラスで呼べる)
protected void LogExport(int count)
=> Console.WriteLine($"{count} 件を {_outputPath} にエクスポートしました");
// abstract メソッド(子クラスが必ず override する)
public abstract void Export(IEnumerable<string> data);
// virtual メソッド(デフォルト実装あり・子クラスが任意で上書き可)
public virtual string GetFormat() => "CSV";
}
class CsvExporter : DataExporter
{
public CsvExporter(string path) : base(path) { }
public override void Export(IEnumerable<string> data)
{
var lines = data.ToArray();
File.WriteAllLines(_outputPath, lines);
LogExport(lines.Length); // 親クラスの protected メソッドを使える
}
}
class JsonExporter : DataExporter
{
public JsonExporter(string path) : base(path) { }
public override void Export(IEnumerable<string> data)
{
var json = System.Text.Json.JsonSerializer.Serialize(data.ToArray());
File.WriteAllText(_outputPath, json);
LogExport(data.Count());
}
public override string GetFormat() => "JSON"; // virtual をオーバーライド
}
DataExporter exporter = new CsvExporter("output.csv");
exporter.Export(new[] { "Alice,25", "Bob,30" });
// 2 件を output.csv にエクスポートしました
テンプレートメソッドパターン(抽象クラスの真骨頂)
抽象クラスの代表的な活用パターンが「テンプレートメソッドパターン」です。処理の骨格(順序・ステップ)を親クラスで定義し、各ステップの具体的な実装を子クラスに任せます。
abstract class ReportGenerator
{
// テンプレートメソッド: 処理の骨格を定義(final 相当として sealed を付ける)
public sealed string Generate(DateTime reportDate)
{
var header = BuildHeader(reportDate); // ①ヘッダー生成(子クラスが実装)
var body = BuildBody(); // ②本文生成(子クラスが実装)
var footer = BuildFooter(); // ③フッター生成(共通処理)
return $"{header}
{body}
{footer}";
}
// 子クラスが実装するステップ
protected abstract string BuildHeader(DateTime date);
protected abstract string BuildBody();
// 共通のフッター(子クラスが任意でオーバーライド可)
protected virtual string BuildFooter()
=> $"--- 生成日時: {DateTime.Now:yyyy-MM-dd HH:mm} ---";
}
class MonthlySalesReport : ReportGenerator
{
private readonly decimal _total;
public MonthlySalesReport(decimal total) => _total = total;
protected override string BuildHeader(DateTime date)
=> $"【月次売上レポート】{date:yyyy年M月}";
protected override string BuildBody()
=> $"売上合計: {_total:C}";
}
class InventoryReport2 : ReportGenerator
{
private readonly int _count;
public InventoryReport2(int count) => _count = count;
protected override string BuildHeader(DateTime date)
=> $"【在庫レポート】{date:yyyy-MM-dd}";
protected override string BuildBody()
=> $"在庫アイテム: {_count:N0} 件";
protected override string BuildFooter()
=> "=== 在庫管理システム ==="; // フッターをカスタマイズ
}
var sales = new MonthlySalesReport(1_250_000);
Console.WriteLine(sales.Generate(new DateTime(2024, 5, 1)));
// 【月次売上レポート】2024年5月
// 売上合計: ¥1,250,000
// --- 生成日時: 2024-05-01 10:30 ---
var inv = new InventoryReport2(3842);
Console.WriteLine(inv.Generate(DateTime.Today));
// 【在庫レポート】2024-05-01
// 在庫アイテム: 3,842 件
// === 在庫管理システム ===
インターフェース vs 抽象クラス:詳細比較
| 項目 | インターフェース | 抽象クラス |
|---|---|---|
| フィールドの宣言 | 不可 | 可(private/protected/public) |
| コンストラクタ | 不可 | 可(子クラスが : base() で呼ぶ) |
| 実装メソッド | C# 8 以降はデフォルト実装が可能 | abstract/virtual/通常の3種すべて可 |
| 多重実装/継承 | 複数実装可(制限なし) | 1クラスのみ継承可 |
| アクセス修飾子 | すべてのメンバーが public(暗黙) | protected など細かく設定可 |
| 直接インスタンス化 | 不可 | 不可(abstract のため) |
| 静的メンバー | C# 8 以降は可・C# 11 で static abstract も可 | 通常の static メンバーが使える |
| 主な用途 | 「何ができるか(能力・ロール)」の定義 | 「何であるか(基盤・共通実装)」の定義 |
どちらを選ぶか:選択の判断基準
以下の判断フローで選択すると迷いにくくなります。
/* ① 状態(フィールド)やコンストラクタが必要か? → YES: 抽象クラス一択(インターフェースはフィールドを持てない) ② 複数の「能力・ロール」を1つのクラスに持たせたいか? → YES: インターフェース(多重実装が必要) ③ 処理の骨格(ステップの順序)を固定したいか? → YES: 抽象クラス(テンプレートメソッドパターン) ④ 異なる継承ツリーのクラス(Car と Dog など)に同じ契約を課したいか? → YES: インターフェース ⑤ DI コンテナやモックで差し替えたいか? → YES: インターフェース(コンストラクタインジェクションの標準的な手法) 迷ったら → インターフェース(後から抽象クラスにするより変更が少ない) */
| シナリオ | 選択 | 具体例 |
|---|---|---|
| フィールド・コンストラクタが必要 | 抽象クラス | protected string _basePath など |
| 複数の能力を1クラスに付与 | インターフェース | IReadable と IWritable を同時実装 |
| 処理ステップの順序を固定 | 抽象クラス | テンプレートメソッドパターン |
| DIで差し替え可能にする | インターフェース | ILogger・IRepository など |
| 全く異なるクラスに同じ契約 | インターフェース | Car と Robot 両方に IMovable |
| protected 共通処理を継承させたい | 抽象クラス | ログ・バリデーション等の共通処理 |
よくある落とし穴と注意点
インターフェースにデフォルト実装を乱用する
C# 8 以降のデフォルト実装はあくまで「既存インターフェースへの後方互換性のある追加」が主な用途です。最初からデフォルト実装に頼って設計すると、実装クラスが「どこの動作が使われているのか」が不明確になります。共通処理を提供したい場合は素直に抽象クラスを選びましょう。
抽象クラスの継承で「継承のためだけの継承」をしてしまう
「親クラスのメソッドを1つだけ使いたい」という理由で継承するのは避けましょう。そのメソッドがなければ成り立たないわけではないなら、それはコンポジション(フィールドとして持つ)で解決すべきです。抽象クラスの継承は「is-a 関係が本当に成り立つとき」に限定すると設計が健全に保たれます。
デフォルト実装はクラス型変数から呼べない
デフォルト実装は実装クラスが override しない限り、インターフェース型の変数からしかアクセスできません。クラス型で受け取ったオブジェクトからデフォルト実装を呼ぼうとするとコンパイルエラーになります。デフォルト実装を多用する設計では、変数の型をインターフェース型で統一する必要があります。
インターフェースの同名メソッドを持つ複数実装で混乱する
同名のメソッドを持つインターフェースを複数実装した場合、明示的インターフェース実装を使わないと意図しない方の実装が呼ばれます。特にサードパーティライブラリのインターフェースを組み合わせるときは名前の衝突に注意し、明示的インターフェース実装でどちらの実装を呼ぶかを明確にしましょう。
よくある質問
virtual は付けられません。C# 8 以降のデフォルト実装は virtual に相当する動作をします(実装クラスが明示的にオーバーライドできる)が、構文上は virtual キーワードを書く必要はありません。static abstract メンバーをインターフェースに定義すると、ボックス化なしにジェネリクスで構造体の能力を利用できます(上述の IAddable<T> パターン)。まとめ
| 特徴 | インターフェース | 抽象クラス |
|---|---|---|
| フィールド | 不可 | 可(private/protected) |
| コンストラクタ | 不可 | 可(子クラスが : base で呼ぶ) |
| 実装メソッド | C# 8以降はデフォルト実装可 | abstract/virtual/通常すべて可 |
| 多重実装/継承 | 複数実装可 | 1クラスのみ |
| 主な用途 | 「何ができるか(能力・ロール)」の定義 | 「何であるか(共通基盤)」の定義 |
| DI・モック | 最適(標準的なパターン) | 継承必須のため差し替えにくい |
| テンプレートメソッド | 不向き(順序の強制が難しい) | 得意(sealed でステップを固定) |
virtual/override の詳細は継承とオーバーライド完全ガイドを、インターフェースを活用した DI の実装は依存性注入(DI)の基本と実装例を参照してください。