C# のインターフェース(interface)は、型が「何ができるか」を表す契約です。実装を持たず(※ C# 8 以降は既定実装を持てますが、原則として契約)、クラスや構造体にその契約を満たす能力を付与します。
抽象クラスと似ているようで設計上の役割は大きく異なり、定義の文法にも独特の制約があります。この記事では、インターフェースとは何か、抽象クラスとの違い、定義時のルール、そしてよく出てくる派生パターンを順に整理します。
インターフェースとは何か
インターフェースは「この型はこのメンバを持つことを保証する」という契約のみを表す型です。フィールドや実装(※既定実装を除く)を持たず、クラス・構造体・レコードがこれを implement(実装)することで契約を満たします。
public interface IPrintable
{
void Print();
}
public class Report : IPrintable
{
public void Print() => Console.WriteLine("レポートを印刷");
}
呼び出し側は具象型ではなくインターフェース越しに操作できるため、実装の差し替えやテストダブルの導入が容易になります。
void Output(IPrintable p) => p.Print();
抽象クラスとの違い
抽象クラス(abstract class)もインスタンス化できず、派生クラスに実装を強制できる点で似ています。しかし設計意図・文法ともに以下のように異なります。
| 観点 | 抽象クラス | インターフェース |
|---|---|---|
| 役割 | 「〜である(is-a)」関係、部分実装の共有 | 「〜できる(can-do)」契約 |
| 多重継承 | 単一のみ | 複数実装可能 |
| フィールド | 持てる | 持てない |
| コンストラクタ | 持てる(protected など) |
非 static のものは持てない |
| アクセス修飾子 | メンバごとに指定可 | メンバは暗黙的に public(C# 8 以降は修飾子指定可) |
| 既定実装 | メソッド本体を書ける | C# 8 以降は既定実装可、ただし制約あり |
| 状態 | 持てる | 基本的に持たない |
ざっくり言えば、共通の「実装」を共有したいときは抽象クラス、共通の「能力」を型横断で付けたいときはインターフェースです。
カスタムインターフェースの定義ルール
インターフェースを自作するときは、次のような文法上・意味上の制約があります。
メンバは暗黙的に public かつ abstract
インターフェース内のメンバは、修飾子を書かなくても public かつ abstract として扱われます。冗長に public abstract を付ける必要はありません(C# 7 以前では書くこともできませんでした)。
public interface IShape
{
// public abstract と書いたのと同じ
double Area();
double Perimeter();
}
C# 8 以降は既定実装のために private / protected / static などを明示できるようになりましたが、通常の契約メンバでは修飾子を省略するのが一般的です。
データフィールドを持てない
インターフェースはインスタンスのデータフィールド(int x; のようなもの)を持てません。状態はあくまで実装側が持ちます。ただし、プロパティは宣言できます(中身の実装は派生側)。
public interface IPerson
{
// OK: プロパティ
string Name { get; set; }
// NG: これはコンパイルエラー
// string name;
}
なお C# 8 以降では static フィールドは持てます(型に紐づく定数的な用途)。
非 static なコンストラクタを持てない
インターフェースには public IShape() {} のようなインスタンスコンストラクタを書けません。そもそもインターフェース自体をインスタンス化できないためです。C# 8 以降は static コンストラクタや static メンバは定義できます。
インターフェース型は new できない
インターフェースは型の形だけを定めたもので、メモリ上の実体を持ちません。よって new IShape() のように直接インスタンス化はできません。必ず具象型(または static ファクトリメソッド)を通して生成します。
// NG
// IShape s = new IShape();
// OK
IShape s = new Circle(1.0);
既定実装(C# 8 以降)
C# 8 から、インターフェースのメソッドに既定実装を書けるようになりました。共通処理を提供しつつ、実装側で上書きもできるという柔軟性が得られます。
public interface ILogger
{
void Log(string message);
// 既定実装
void LogError(string message) => Log($"[ERROR] {message}");
}
ただしこれはあくまで「共通の既定挙動」の提供であり、抽象クラスの代替ではありません。状態を持たせたい場合は引き続き抽象クラスの出番です。
命名規則
慣習として、インターフェース名は大文字の I で始めます(IEnumerable, IDisposable, IComparable)。プロジェクト内でも例外なくこの規則に従うのが読みやすさにつながります。
派生パターン
インターフェースを「使う」「作る」際に登場する典型的なパターンを整理します。C# では複数インターフェースの実装が可能で、さらに基底クラスとの組み合わせも自由です。
クラスが単一インターフェースから派生する
最も基本的なパターンです。クラス名の後ろに : IXxx と書きます。
public interface IGreeter
{
string Greet();
}
public class JapaneseGreeter : IGreeter
{
public string Greet() => "こんにちは";
}
System.Object と同時に(暗黙に)派生
C# のすべての参照型は暗黙に System.Object を継承します。インターフェースを実装する場合も例外ではありません。
public class Foo : IGreeter
{
public string Greet() => "hi";
}
// 以下と等価
// public class Foo : Object, IGreeter { ... }
したがってインターフェース越しに持っていても、ToString() や GetHashCode() といった object のメンバは常に呼び出せます。
カスタム基底クラス+インターフェース
基底クラス(1 つだけ)と複数のインターフェースを同時に継承・実装できます。順序はコンパイラが区別するため、基底クラスを先に書く必要があります。
public abstract class Animal
{
public abstract string Name { get; }
}
public interface IWalkable { void Walk(); }
public interface ISwimmable { void Swim(); }
public class Duck : Animal, IWalkable, ISwimmable
{
public override string Name => "アヒル";
public void Walk() => Console.WriteLine("歩く");
public void Swim() => Console.WriteLine("泳ぐ");
}
複数のインターフェースから派生
クラスの多重継承はできませんが、インターフェースは複数実装できます。異なる観点の「能力」を自由に組み合わせられるのがインターフェースの強みです。
public interface IReadable { string Read(); }
public interface IWritable { void Write(string s); }
public class File : IReadable, IWritable
{
public string Read() => "...";
public void Write(string s) { /* ... */ }
}
インターフェースがインターフェースから派生
インターフェース同士の継承もできます。派生インターフェースを実装すると、基のインターフェースのメンバも実装する必要があります。
public interface IReadable
{
string Read();
}
public interface IReadWritable : IReadable
{
void Write(string s);
}
public class Memo : IReadWritable
{
public string Read() => "メモ";
public void Write(string s) { /* ... */ }
}
さらに、インターフェースは複数のインターフェースから同時に派生できます。
public interface IDuplex : IReadable, IWritable { }
struct がインターフェースから派生
インターフェースはクラスだけでなく構造体(struct)や record でも実装できます。値型でも契約を満たせるのが C# の型システムの特徴です。
public interface IIdentifiable
{
int Id { get; }
}
public struct UserId : IIdentifiable
{
public int Id { get; }
public UserId(int id) => Id = id;
}
ただし、struct を interface 型の変数に代入するとボクシングが発生する点に注意が必要です。パフォーマンスが重要な場面ではジェネリック制約 where T : IIdentifiable を使うとボクシングを避けられます。
void Print<T>(T value) where T : IIdentifiable
=> Console.WriteLine(value.Id); // boxing しない
明示的インターフェース実装
複数インターフェースが同名メンバを持つときや、実装を隠したいときは明示的実装を使えます。
public interface IEnglish { string Greet(); }
public interface IJapanese { string Greet(); }
public class Bilingual : IEnglish, IJapanese
{
string IEnglish.Greet() => "Hello";
string IJapanese.Greet() => "こんにちは";
}
// 呼び出し側はインターフェース経由でのみアクセス可能
IEnglish e = new Bilingual();
Console.WriteLine(e.Greet()); // Hello
明示的実装されたメンバはクラスの public な API からは見えなくなり、インターフェース越しでのみ呼び出せます。
使いどころの指針
- 型横断の「能力」を定めたい → インターフェース
- 実装の共有と「is-a」関係を表したい → 抽象クラス
- 両方ほしい → 抽象クラス+インターフェースの併用(例:
Streamは抽象クラスだがIDisposableを実装) - 値型にも適用したい → インターフェース(ジェネリック制約と組み合わせるとなお良い)
インターフェースは「疎結合」「差し替え可能性」「テスト容易性」を支える中心的な仕組みです。まずは小さく、必要な契約だけを表す薄いインターフェースを定義することを意識すると、設計が大きく崩れにくくなります。