インターフェースは契約で、型が用意することを約束するメンバーに名前を付けた一覧です。型が何をできるかを述べ、どうやるかについては何も述べません。クラスと構造体がインターフェースを実装し、インターフェースに対して書いたコードは、後から書かれるものも含め、すべての実装で動きます。
インターフェースの宣言と実装
インターフェースは本体のないメンバーを宣言します。慣例として、名前は I で始めます。クラスはコロンの後にそれを並べ、各メンバーに対して公開のメンバーを用意することで実装します。
出力:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice は PercentOff についても FixedOff についても何も知りません。依存しているのは IDiscount だけなので、後から BuyOneGetOne クラスを追加しても変更は不要です。この疎結合こそ、インターフェースが存在する理由のすべてです。
宣言についてのいくつかのルール:
- インターフェースのメンバーは既定でpublicで、実装するメンバーも(後述の明示的な実装でない限り)publicでなければなりません。
- インターフェースは、メソッド、プロパティ、インデクサー、イベントを宣言できます。インスタンスフィールドやコンストラクターは宣言できません。
- メンバーを省いたクラスはコンパイルできません。エラーCS0535「'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'」です。
newでインターフェースを作ることはできません。実装するクラスを作り、インターフェースの型の変数に保持できます:IDiscount d = new FixedOff(5);。
型としてのインターフェース
変数、引数、フィールド、戻り値の型はインターフェースとして宣言できます。すると、それを実装する任意のオブジェクトを受け入れ、インターフェースのメンバーだけを公開します。これが、どの実装を受け取るかを気にしないコードを書く方法であり、テストで偽物に差し替える方法です。
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
ASP.NET Coreの依存性の注入のコンテナーはこの上に作られていて、サービスはインターフェースによって登録され、要求されます。
複数のインターフェースを実装する
クラスの基底クラスは多くても1つですが、インターフェースはいくつでも実装できます。基底クラスを先に書き、その後にインターフェースをカンマで区切って並べます。
出力:
INVOICE Studio rent March: 950.00
True
True
インターフェースは他のインターフェースを継承することもできます。interface IRepository<T> : IReadRepository<T> は継承したメンバーにメンバーを追加し、IRepository<T> を実装するクラスは両方の集まりを用意しなければなりません。
明示的なインターフェースの実装
2つのインターフェースが同じ名前で異なる意味のメンバーを宣言していることや、あるインターフェースのメンバーがクラス自身の公開された面では意味をなさないことがあります。インターフェースの名前を前に付け、アクセス修飾子を省いて、明示的に実装します。
出力:
Desk lamp,34.90
Desk lamp for 34.90
明示的に実装したメンバーには、インターフェースを通してしかアクセスできません。フレームワークはこれを使って、よく使う型から余計なものを外しています。配列は IList.Add を明示的に実装している(配列はサイズが固定なので NotSupportedException を投げます)ので、array. と入力してもそのメソッドは表示されません。
実装することになるフレームワークのインターフェース
標準のインターフェースを実装すると、自分の型が既存のフレームワークのコードとつながります。よく現れるものが3つあります。
IComparable<T> は型に自然な順序を与え、List<T>.Sort()、Array.Sort、Max() がそれを使います。CompareTo は負の数、ゼロ、正の数のいずれかを返します。
出力:
1.4, 2.9, 2.10, 10.0
同じバージョンを文字列として並べ替えると 1.4, 10.0, 2.10, 2.9 になります。IComparable<T> がないと、2つの Version オブジェクトを比較する方法がないので、Sort() は InvalidOperationException を投げます。
IEnumerable<T> は、型を foreach とLINQで使えるようにします。GetEnumerator を実装する簡単な方法は yield return で、IEnumerableとyieldのページで扱います。
IDisposable は、解放しなければならないもの(ファイルハンドル、接続、タイマー)を保持する型を表します。その唯一のメソッド Dispose は、例外が投げられた場合でも、ブロックの終わりで using ステートメントが呼ぶものです。
出力:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
既定のインターフェースメソッド(C# 8)
公開済みのインターフェースにメンバーを追加すると、かつてはそれを実装するすべてのクラスが壊れました。C# 8以降、インターフェースのメンバーは本体を持てるようになり、実装するクラスは独自のものを用意しない限りそれを継承します。
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
最後の行は多くの人が驚く点です。既定のメソッドはクラス自身のメンバーには継承されないので、インターフェースの型を通してしか呼べません。既定のメソッドはインスタンスフィールドも使えず(インターフェースにはありません)、インターフェースの他のメンバーを通して動作します。.NET Core 3.0以降が必要で、.NET Frameworkでは使えません。
C# 8ではインターフェースに静的メンバーも許されるようになり、C# 11では静的抽象メンバーが追加されて、ジェネリックなコードから T の静的メソッドや演算子を呼べるようになりました。.NET 7のジェネリックな数学(INumber<T>、where T : INumber<T>)はこの上に作られています。
インターフェースか抽象クラスか
無関係な型が共有できる能力、構造体も参加すべき場合、クラスがそうした役割をいくつも必要とする場合はインターフェースを使います。実装が1つのものの変種で、状態や固定のアルゴリズムを共有する場合は抽象クラスを使います。抽象クラスのページに並べた比較表があります。多くの設計では両方を使います。利用する側のためのインターフェースと、実装する側のために退屈な部分を実装する抽象基底クラスです。
よくある間違い
- 実装がない、またはpublicでない。 インターフェースのすべてのメンバーには、シグネチャの一致するpublicなメンバーか、明示的な実装が必要です。そうでなければCS0535(メソッドは存在するがpublicでない場合はCS0737)になります。
- 「念のため」の実装が1つだけのインターフェース。 インターフェースが存在価値を持つのは、実装が複数あるか、テスト用の偽物がある場合です。そうでなければ、たどるファイルが1つ増えるだけです。
- 肥大化したインターフェース。 メンバーが20個あるインターフェースは、実装するすべての型に20個のメンバーを書かせます。クラスが組み合わせられる小さな役割(
IReader、IWriter)に分けます。 - インターフェースをクラスにキャストし直す。
((StripeGateway)gateway).Refund()は疎結合を台無しにします。呼び出し側がRefundを必要とするなら、それはインターフェースに属します。 - 既定のインターフェースメソッドをクラスで呼べると思う。 インターフェースの型を通してアクセスします。
よくある質問
C#のインターフェースとは何ですか?
インターフェースは、型が用意することを約束するメンバー(メソッド、プロパティ、イベント、インデクサー)に名前を付けた集まりで、インスタンスの状態を持ちません。interface IDiscount { decimal Apply(decimal price); } は「IDiscountであるものは何でも、自分を価格に適用できる」と述べています。クラスと構造体はコロンでそれを実装し、コードはインターフェースの型を通してそのどれとでも動けます。
C#でインターフェースを実装するには?
クラスの宣言のコロンの後に(基底クラスがあればその後に)それを並べ、インターフェースの各メンバーに対して公開のメンバーを用意します:class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }。メンバーが足りないとエラーCS0535になります。Visual StudioとRiderはクイックフィックスで雛形を生成できます。
C#のクラスは複数のインターフェースを実装できますか?
できます。必要なだけカンマで区切って並べます:class Invoice : IPrintable, IComparable<Invoice>, IDisposable。これが多重継承に対するC#の答えで、クラスの基底クラスは1つですが、多くの役割を担えます。2つのインターフェースが同じメンバーを宣言している場合は、1つの公開メソッドで両方を満たすか、明示的な実装でそれぞれに独自のものを与えられます。
C#の明示的なインターフェースの実装とは何ですか?
インターフェースの名前を前に付け、アクセス修飾子なしでメンバーを実装することです:string IExportable.Format() { ... }。そのメンバーはクラスからではなく、インターフェースの型の変数を通してしか呼べなくなります。2つのインターフェースの名前の衝突を解決したり、めったに使わないインターフェースのメンバーをクラスの公開された面から外したりするために使います。
C#の既定のインターフェースメソッドとは何ですか?
C# 8以降、インターフェースのメンバーは本体を持てます:void Error(string m) => Write("ERROR: " + m);。実装するクラスはそれを無償で得て、独自のものを用意することもできます。この機能は、ライブラリの作者が公開済みのインターフェースに、すべての実装を壊さずにメンバーを追加できるようにするために存在します。既定のメソッドには、クラスではなくインターフェースの型を通してアクセスします。
C#のインターフェースの名前がIで始まるのはなぜですか?
.NETの命名規則だからです(IEnumerable、IDisposable、IComparable<T>)。大文字のIの後にPascalCaseの名前を続け、多くの場合は「-able」で終わる形容詞にします。コンパイラは要求しませんが、従っておくと、特に基底クラスとインターフェースが1つのリストに並ぶクラスの宣言で、インターフェースの型がひと目でわかります。