【C#】インターフェースと抽象クラス完全ガイド|違い・使い分け・デフォルト実装・明示的実装・設計パターンまで

インターフェースと抽象クラスは 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"));
インターフェースを使うと実装を差し替え可能にできます。テスト時にモック実装を渡す・本番でSMTPを使い開発ではコンソール出力にする、といった切り替えが容易になります。これが依存性注入(DI)の基盤になります。

複数インターフェースの実装(多重能力の付与)

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);
「継承(is-a 関係)」は1つのクラスしか選べませんが、インターフェース(can-do / role)は何個でも組み合わせられます。クラス設計では「何である(クラス継承)」よりも「何ができる(インターフェース)」を軸にすると柔軟な設計になります。

インターフェースの新機能(C# 8以降)

デフォルト実装(Default Interface Methods)

C# 8 から、インターフェースのメソッドにデフォルト実装を書けるようになりました。実装クラスがそのメソッドをオーバーライドしなければ、デフォルト実装が使われます。主に既存のインターフェースに後から機能を追加するときの互換性維持に使います。

デフォルト実装(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 メンバーをインターフェースで宣言できます。ジェネリクスと組み合わせて、型パラメーター自体の静的メソッドを制約できます。

static abstract メンバー(C# 11)
// 数値型の加算を抽象化する例
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クラスに付与 インターフェース IReadableIWritable を同時実装
処理ステップの順序を固定 抽象クラス テンプレートメソッドパターン
DIで差し替え可能にする インターフェース ILoggerIRepository など
全く異なるクラスに同じ契約 インターフェース CarRobot 両方に IMovable
protected 共通処理を継承させたい 抽象クラス ログ・バリデーション等の共通処理

よくある落とし穴と注意点

インターフェースにデフォルト実装を乱用する

C# 8 以降のデフォルト実装はあくまで「既存インターフェースへの後方互換性のある追加」が主な用途です。最初からデフォルト実装に頼って設計すると、実装クラスが「どこの動作が使われているのか」が不明確になります。共通処理を提供したい場合は素直に抽象クラスを選びましょう。

抽象クラスの継承で「継承のためだけの継承」をしてしまう

「親クラスのメソッドを1つだけ使いたい」という理由で継承するのは避けましょう。そのメソッドがなければ成り立たないわけではないなら、それはコンポジション(フィールドとして持つ)で解決すべきです。抽象クラスの継承は「is-a 関係が本当に成り立つとき」に限定すると設計が健全に保たれます。

デフォルト実装はクラス型変数から呼べない

デフォルト実装は実装クラスが override しない限り、インターフェース型の変数からしかアクセスできません。クラス型で受け取ったオブジェクトからデフォルト実装を呼ぼうとするとコンパイルエラーになります。デフォルト実装を多用する設計では、変数の型をインターフェース型で統一する必要があります。

インターフェースの同名メソッドを持つ複数実装で混乱する

同名のメソッドを持つインターフェースを複数実装した場合、明示的インターフェース実装を使わないと意図しない方の実装が呼ばれます。特にサードパーティライブラリのインターフェースを組み合わせるときは名前の衝突に注意し、明示的インターフェース実装でどちらの実装を呼ぶかを明確にしましょう。

よくある質問

Qインターフェースと抽象クラス、どちらを先に検討すべきですか?
A一般的にはインターフェースを先に検討します。インターフェースはより少ない制約(多重実装可能・継承ツリーに依存しない)で設計できます。抽象クラスに切り替えるのは「共通フィールドが必要」「コンストラクタを共有したい」「テンプレートメソッドパターンを使いたい」という具体的な必要性が生じたときです。
QC# 8 のデフォルト実装があるなら抽象クラスは不要になりましたか?
Aそうではありません。デフォルト実装は「既存インターフェースへの後方互換追加」が主目的です。デフォルト実装には①フィールドを持てない②コンストラクタを呼べない③デフォルト実装はクラス型変数から直接呼べない、という制約があります。共通状態・初期化処理・テンプレートメソッドパターンが必要な場面では抽象クラスが依然として最適です。
Qインターフェースのメソッドに virtual は付けられますか?
Aインターフェースのメソッドは暗黙的に「実装クラスがオーバーライドするもの」なので virtual は付けられません。C# 8 以降のデフォルト実装は virtual に相当する動作をします(実装クラスが明示的にオーバーライドできる)が、構文上は virtual キーワードを書く必要はありません。
Q抽象クラスをインターフェースとして使い回す設計は良いですか?
A推奨しません。DI コンテナや外部ライブラリとの連携では通常「インターフェース型」でバインドします。抽象クラスは継承でしか使えないため、異なる継承ツリーのクラスには適用できません。外部に公開する「契約」はインターフェース、内部の実装基盤は抽象クラス、という役割分担で設計するのがベストプラクティスです。
Q構造体(struct)はインターフェースを実装できますか?
Aはい、実装できます。ただし構造体は値型であり、インターフェース型の変数に代入する(アップキャスト)とボックス化(ヒープへのコピー)が発生してパフォーマンスに影響します。C# 11 の static abstract メンバーをインターフェースに定義すると、ボックス化なしにジェネリクスで構造体の能力を利用できます(上述の IAddable<T> パターン)。

まとめ

特徴 インターフェース 抽象クラス
フィールド 不可 可(private/protected)
コンストラクタ 不可 可(子クラスが : base で呼ぶ)
実装メソッド C# 8以降はデフォルト実装可 abstract/virtual/通常すべて可
多重実装/継承 複数実装可 1クラスのみ
主な用途 「何ができるか(能力・ロール)」の定義 「何であるか(共通基盤)」の定義
DI・モック 最適(標準的なパターン) 継承必須のため差し替えにくい
テンプレートメソッド 不向き(順序の強制が難しい) 得意(sealed でステップを固定)

virtual/override の詳細は継承とオーバーライド完全ガイドを、インターフェースを活用した DI の実装は依存性注入(DI)の基本と実装例を参照してください。