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を継承しつつ、複数のインターフェース(IDrivable、IRepairable、IInsurable)を実装できる。もし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を隠したい時は便利だけど、初心者にはちょっと意外かも。
GO TO FULL VERSION