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以前 | |
| C# 8以降 | |
構文の重要ポイント
- 実装付きメソッドには必ず波カッコで本体を書くこと!
- デフォルトメソッドは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();
}
}
やりすぎ注意!
デフォルトメソッドは後方互換性を守るのに便利だけど、使いすぎるとアーキテクチャが「ごちゃごちゃ」になって、ロジックがインターフェースに散らばっちゃう。大事なロジックはクラスに置いて、インターフェースは本当に「契約」として使うのがオススメ!
GO TO FULL VERSION