CodeGym /コース /C# SELF /インターフェースと抽象クラスの比較

インターフェースと抽象クラスの比較

C# SELF
レベル 24 , レッスン 2
使用可能

1. クラシックな違いをざっくり

誰かが「インターフェースはただのシグネチャの集まりだよ」って言ったら、「どのC#バージョンで書いてるの?」って聞いてみて。C# 8以降、インターフェースはめっちゃパワーアップしたんだ。そろそろ抽象クラスと比べてみよう ― 昔ながらの特徴だけじゃなく、.NETの新機能も含めてね。

数年前、C# 7の時代に戻ると、話はシンプルだった。抽象クラスはフィールドや部分的に実装されたメソッドを持てるけど、インターフェースはシグネチャ(メソッド、プロパティ、イベント、インデクサ)だけ。
抽象クラスの継承は"is-a"Is-a)関係、インターフェースは複数の振る舞いの継承("can-do"、can-do)。

特徴 抽象クラス インターフェース(C# 8以前)
関係性 is-a can-do
継承 1つだけ 複数可
フィールド 持てる 持てない
メソッド実装 できる できない
コンストラクタ 持てる 持てない
アクセス修飾子 いろいろ(public, protected, ...) 暗黙的にpublicのみ

見ての通り、昔は抽象クラスがインターフェースの「兄貴分」って感じで、パワフルで柔軟だった。でも時代は変わる!

2. デフォルト実装付きインターフェース

C# 8(そしてC# 14や.NET 9ではなおさら)から、インターフェースは新しいスーパーパワー ― デフォルト実装付きメソッド(Default Interface Methods、DIM)を手に入れたんだ。

どんな感じ?


public interface IAnimal
{
    void SayHello();

    // デフォルト実装付きメソッド!
    void Walk()
    {
        Console.WriteLine("オレ歩いてるよ…");
    }
}

すごっ! いきなりインターフェースでも実装が書けるようになった。しかも何個でもOK。ただし注意点:こういうメソッドは必ず本体付きで宣言しなきゃダメ。他は(フィールド、privateメソッド、コンストラクタ)は今でもダメ。

3. 今どきインターフェースの機能

.NETエンジニアなら知っておきたい新機能:

  • デフォルト実装付きメソッド
  • インターフェース内のprivateメソッド(補助用、同じインターフェース内だけで使える)
  • staticメソッド(C# 8以降)
  • デフォルト実装付きプロパティ
  • staticフィールド(C# 14以降 ― "static interface members")
  • abstract staticメンバー("abstract static members" ― インターフェースが実装側にstaticメソッドを要求できるようになった!)

今どきインターフェースのフル例:


public interface ILogger
{
    static int LoggerCount { get; set; } // C# 14

    void Log(string message); // シグネチャ(契約)

    // デフォルト実装
    void LogWarning(string warning)
    {
        Log("[WARNING]: " + warning);
    }
    
    // インターフェース内のprivate補助メソッド(C# 8+)
    private void FormatAndLog(string level, string msg)
    {
        Log($"{level}: {msg}");
    }

    // インターフェース内のstaticメソッド(C# 8+)
    static void PrintLoggerInfo()
    {
        Console.WriteLine("ILoggerインターフェース ― 最高の相棒だよ!");
    }
}

想像してみて ― 昔はこんなの絶対ムリだった。猫がサーバーガードやるくらいありえなかったよ。

4. 抽象クラス:最近どう?

抽象クラスは…うーん、ここ10年であんまり進化してない。今でもできることは:

  • フィールド(private/protected/staticもOK)
  • 実装済み・抽象メソッド
  • コンストラクタ(初期化ロジックも書ける)
  • プロパティ、イベント、インデクサ
  • static/インスタンスメンバー

抽象クラスの例:


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

    public abstract void Speak();

    public virtual void Walk()
    {
        Console.WriteLine($"{Name}は足で歩いてるよ!");
    }

    protected void Eat()
    {
        Console.WriteLine($"{Name}はごはんを食べてる。");
    }
}

抽象クラスは今でも、共通ロジックや状態、振る舞いをクラス階層でまとめるのに最適な場所だよ。

5. 今どき比較:新機能込みの表

特徴 抽象クラス インターフェース(C# 14+、.NET 9)
関係性 is-a(〜である) can-do(〜できる)
継承 1つだけ 複数可
フィールド OK、なんでも staticのみ*(C# 14+)
コンストラクタ OK NG
メソッド実装 OK(virtual/abstract OK(default, static, abstract static)
実装付きプロパティ OK OK(default implementation)
privateメンバー OK OK(メソッドのみ、C# 8+)
staticメンバー OK OK(C# 8+、制限あり)
staticフィールド OK OK *(C# 14+)
アクセス修飾子 なんでも デフォルトはpublicかprivate

* ― インターフェースのstaticフィールドは特殊なケースで使うことが多く、かなり新しい機能だよ。

6. どっちを使う?今どきのおすすめ

インターフェース(しかも今はデフォルト実装付き)は、契約を作るための道具。最大の特徴は複数実装。クラスは10個でも20個でもインターフェースを実装できるから、まさに万能戦士。

抽象クラスを選ぶのはこんな時:

  • 共通の状態(フィールド)、ロジック、振る舞いを継承したい時
  • 標準だけどオーバーライド可能なロジックが欲しい時(virtualを使おう)
  • コンストラクタで初期化を集中管理したい時

実際のプロジェクトでは、こんなパターンが多いよ:「純粋な契約」はインターフェースで書いて、共通コードやインフラが必要なら抽象基底クラスを作る。


.                     ┌────────────────────────┐
                      │      インターフェース      │
                      │  (契約:何ができるか)     │
                      └─────────┬──────────────┘
                                │
                 ┌──────────────┼──────────────┐
                 │              │              │
           実装1           実装2 ...        実装N
             MyLogger     CloudLogger     FileLogger
    
        (抽象クラスの継承と組み合わせてもOK)
図:どこで何を使う?

7. シナリオ ― どっちが有利?

複数実装:
たとえば、IDrivableインターフェースとVehicle抽象クラスがあるとする。CarクラスはベースとしてVehicleを継承しつつ、複数のインターフェース(IDrivableIRepairableIInsurable)を実装できる。もしRepairableが抽象クラスだったら、VehicleかRepairableかどっちかしか選べない!インターフェースの勝ちだね。

共通ロジック・状態:
たとえば、すべての「乗り物」に「ナンバー」フィールドが必要なら、それは抽象クラスのフィールドにすべき。インターフェースには(static以外の)フィールドは持てないからね。

API進化:
Default Interface Methodsの革命的な話 ― これでインターフェースを進化させても、既存の利用者を壊さずに済む。
たとえば、インターフェースに新しいデフォルト実装付きメソッドを追加しても、全部の既存実装(インターフェース実装クラス)はそのまま動く!昔はこれ、痛かった(てか、無理だった)。

8. 実践例

うちの学習アプリにもだんだんロギングが増えてきた。じゃあ、デフォルト実装付きのILoggerインターフェースを作ってみよう:


public interface ILogger
{
    void Log(string message);

    // デフォルト実装は全部の実装クラスで使える!
    void LogInfo(string info)
    {
        Log("[INFO] " + info);
    }

    // インターフェースのstaticメソッド
    static void PrintHelp()
    {
        Console.WriteLine("ILoggerを使ってイベントをロギングしよう");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

// どこかのコードで:
ILogger logger = new ConsoleLogger();
logger.LogInfo("システム起動!"); // default実装のおかげで動く

// インターフェースのstaticメソッド呼び出し
ILogger.PrintHelp();

もしインターフェースに新しいデフォルト実装付きメソッドを追加しても、既存の実装(たとえばConsoleLogger)は自動的にその新メソッドを使える ― コードが壊れる心配なし。

9. 注意点・落とし穴:実践でのハマりどころ

全部バラ色ってわけじゃない。たとえば、インターフェースにdefault実装があっても、クラス型でオブジェクトを扱うとdefault実装は使えない。インターフェース型でアクセスしないとダメ。


ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // コンパイルエラー:LogInfoはクラスに定義されてない!

ILogger log2 = log;
log2.LogInfo("Hello"); // これはOK!

これはインターフェースの明示的実装の一種みたいなもの。余計なAPIを隠したい時は便利だけど、初心者にはちょっと意外かも。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION