CodeGym /コース /C# SELF /インターフェースのDefaultメソッド

インターフェースのDefaultメソッド

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

1. 「ピュアな契約」から柔軟なアーキテクチャへ

C# 8以前は、インターフェースはガチガチの契約だったんだ。インターフェースを実装したいなら、最後のカンマまで全部実装しなきゃダメ。もし新しいメンバーを追加したら、既存の実装全部に即座に追加しないと、コンパイラがビルドを許してくれなかった。

でも現実はそんなに単純じゃないよね。例えば、何百ものプロジェクトで使われてるライブラリをメンテしてて、急にインターフェースに新しいメソッドを追加したくなったとする。後方互換性を壊したくないよね?そこで登場するのがデフォルトメソッドDefault Interface Methods, DIM)!

何がポイント?

デフォルトメソッドを使うと、実装をインターフェースの中に直接書けるんだ。これで契約がもっと柔軟になる。もしクラスが「新入り」を実装してなくても、デフォルトの実装が使われる。映画のスタントマンみたいなもんで、俳優が橋から飛び降りたくなければ、スタントマンが代わりにやってくれる感じ!

2. Default Interface Methodsの構文

インターフェース内で実装付きメソッドをどう宣言する?

普通のメソッドとほぼ同じだけど、今度はメソッドの本体をインターフェースの中に(しかも必ず!)書けるようになった:


public interface ILogger
{
    void Log(string message);

    // 新しいデフォルト実装付きメソッド!
    void LogWarning(string message)
    {
        Log("[WARNING] " + message);
    }
}

ここでLogWarningはもう実装済み!ILoggerを実装するクラスはLogだけ実装すればOK、LogWarningは(自前で用意しなければ)デフォルト実装が使われるよ。

比較:クラシック vs. モダンなシグネチャ

バージョン インターフェースでの宣言
C# 8以前
void DoSomething();
C# 8以降
void DoSomething() { Console.WriteLine("動作中!"); }

構文の重要ポイント

  • 実装付きメソッドには必ず波カッコで本体を書くこと!
  • デフォルトメソッドはabstractにはできない。
  • インターフェースのメソッドは今まで通り暗黙的にpublic
  • get/set付きのプロパティもデフォルト実装できる(下記参照)。

3. 実践サンプル

サンプル1. 後方互換性を守る

例えば、アプリにデータ保存用インターフェースがあるとする:


public interface ISaveable
{
    void Save(string filePath);
}

後でクラウド保存も追加したくなった。でも100個のクラス全部修正したくない?デフォルトメソッドを追加しよう!


public interface ISaveable
{
    void Save(string filePath);

    // 新しい「デフォルト」実装付きメソッド!
    void SaveToCloud(string cloudService)
    {
        Console.WriteLine($"クラウド{cloudService}に保存中(デフォルトでは何もしない)");
    }
}

これで古いクラスも自動的に「クラウド保存できる」ようになる(今はメッセージ出すだけだけど)。

サンプル2. ロガーインターフェースを拡張

前はシンプルなロギングインターフェースだった:


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

エラーログ用のデフォルトメソッドを追加しよう:


public interface ILogger
{
    void Log(string message);

    void LogError(string message)
    {
        Log("[ERROR] " + message);
    }
}

ILoggerを実装するクラスはLogErrorを実装しなくてもOK—デフォルトバージョンが動く:


public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
    // LogErrorは実装しない—デフォルト実装が使われる!
}

ILogger logger = new ConsoleLogger();
logger.Log("全部OK!");
logger.LogError("あれれ、なんかおかしい!"); // デフォルト実装が呼ばれる

サンプル3. デフォルトメソッド+拡張可能アプリ

アプリがいろんなエクスポートタイプ(ファイル、DB、ネットワーク)をサポートしてるとする。インターフェースは:


public interface IExporter
{
    void Export(string data, string destination);

    // 新機能—アーカイブへのエクスポート
    void ExportToArchive(string data, string archivePath)
    {
        Console.WriteLine("デフォルトではアーカイブ未対応。");
    }
}

他の開発者が書いたプラグインも、新しいメソッドを知らなくてもそのまま動くよ。

4. デフォルトインターフェースメソッドの呼び出しはどう動く?

「古いクラス—新しいインターフェース」シナリオ

クラスがデフォルトメソッドを実装してなければ、インターフェース経由で呼ぶとインターフェースの実装が使われる。実装してれば自分のが使われる。


public class FileExporter : IExporter
{
    public void Export(string data, string destination)
    {
        Console.WriteLine("ファイルに保存中...");
    }
    // ExportToArchiveは実装しない—デフォルト出力
}

IExporter exporter = new FileExporter();
exporter.Export("データ", "file.txt");        // FileExporterの実装が動く
exporter.ExportToArchive("データ", "file.zip"); // デフォルト実装が動く!

「クラスがデフォルトメソッドをオーバーライド」シナリオ


public class AdvancedExporter : IExporter
{
    public void Export(string data, string destination)
    {
        Console.WriteLine("アドバンスモードで保存中...");
    }

    public void ExportToArchive(string data, string archivePath)
    {
        Console.WriteLine("アーカイブ対応してるよ!");
    }
}

IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("データ", "file.zip"); // 今度はクラスの実装が呼ばれる!

5. Default Interface Methodsで他にできること

デフォルトプロパティやイベント

getやsetの本体があれば、デフォルト実装付きプロパティも宣言できる:


public interface IHasId
{
    // オーバーライドされるまで自動で42を返す
    int Id => 42;
}

public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42

インターフェース内でデフォルトメソッドを呼ぶ

インターフェースの中で、デフォルトメソッドや他のメンバー同士を呼び合える:


public interface IDemo
{
    void Foo() => Bar();
    void Bar() => Console.WriteLine("BAR");
}

6. Default Interface Methodsの制限と特徴

フィールドやコンストラクタは宣言できる?

無理!デフォルトメソッドがあってもインターフェースはクラスじゃない。フィールド、コンストラクタ、デストラクタは持てないよ。

baseをインターフェースに使える?

使えるけど、ちょっとクセがある。デフォルトメソッド内で親インターフェースのメソッドを明示的に呼び出せる:


public interface IBase
{
    void Greet() => Console.WriteLine("IBaseからこんにちは");
}

public interface IDerived : IBase
{
    void IBase.Greet()
    {
        Console.WriteLine("IDerivedからこんにちは!");
        IBase.Greet(this); // 親インターフェースのメソッドを明示的に呼ぶ
    }
}

でも基本的な使い方ではあまり必要ないかな。

デフォルト実装が衝突したら?


public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }

// クラスがFooを明示的に実装しない場合:
public class C : IA, IB { }
// コンパイルエラー:どっちの実装を使うか不明!

7. よくあるミス・制限・特徴

ありがちなミス:
DIMを知った後、インターフェースにフィールドを宣言しようとする人がいるけど、やっぱり無理。あと、デフォルトのstaticメソッドを実装しようとすると—C# 8以前は不可。8以降はstaticメソッド自体はOKだけど、デフォルト実装付きstaticメソッドはまた別の話。

特徴:ダイヤモンド問題(Diamond Problem)
もしクラスが同じデフォルトメソッドを持つ2つのインターフェースを実装したら、そのメソッドを自分で明示的に実装しなきゃダメ:


public class ConflictClass : IA, IB
{
    public void Foo() // どっちを使うか自分で決める!
    {
        // 必要ならインターフェース経由で明示的に呼び出し
        ((IA)this).Foo();
        // または
        ((IB)this).Foo();
    }
}

やりすぎ注意!
デフォルトメソッドは後方互換性を守るのに便利だけど、使いすぎるとアーキテクチャが「ごちゃごちゃ」になって、ロジックがインターフェースに散らばっちゃう。大事なロジックはクラスに置いて、インターフェースは本当に「契約」として使うのがオススメ!

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