複数のインターフェイスを実装
C#は多重継承を認めていません(1つのクラスしか継承できない)。この制約はクラスに対してのみかかります。すなわち、インターフェイスは複数実装できます。
例えば、以下のような型を作れます。
struct Id : IComparable<Id>, IEquatable<Id>
{
public int Value { get; set; }
public int CompareTo(Id other) => Value.CompareTo(other.Value);
public bool Equals(Id other) => Value == other.Value;
}
型引数違いのジェネリック インターフェイス
C#では、オーバーロード解決ができる限り、同名のメンバーを持つインターフェイスを複数、普通に実装することができます(オーバーロード解決できない場合には、次節の明示的実装が必要になります)。
これは特に、ジェネリックなインターフェイスを、型引数違いで複数実装する際に有効です。
例えば、標準ライブラリのIEquatable<T>
インターフェイス(System
名前空間)について、異なる型引数で複数実装できます。
A
とB
という2つのクラスがあったとして、IEquatable<A>
とIEquatable<B>
という2つの実装を持てます。
具体的な用途としては、例えば、以下のような場面で有効です。
- 図形全般を表す
Shape
型がある -
Shape
から派生した、矩形型Rectangle
があるRectangle
は、幅と高さの両方の比較で等値判定する
-
Shape
から派生した、円型Circle
があるCircle
は、半径の比較で等値判定する
Shape
は、矩形同士、円同士でだけ等値判定をする。型が違う場合はその時点で不一致
この条件下では、それぞれのクラスに以下のようにインターフェイスを持てます。
Shape
は他のShape
と比較できるので、IEquatable<Shape>
を実装できる-
Rectangle
は他のRectangle
と比較できるので、IEquatable<Rectangle>
を実装できるRectangle
はShape
から派生しているので、IEquatable<Shape>
でもある
-
Circle
は他のCircle
と比較できるので、IEquatable<Circle>
を実装できるCircle
はShape
から派生しているので、IEquatable<Shape>
でもある
これを、以下のようなコードで実装できます。
using System;
abstract class Shape : IEquatable<Shape>
{
public abstract bool Equals(Shape other);
}
class Rectangle : Shape, IEquatable<Rectangle>
{
public double Width { get; set; }
public double Height { get; set; }
public override bool Equals(Shape other) => Equals(other as Rectangle);
public bool Equals(Rectangle other)
=> other != null && Width == other.Width && Height == other.Height;
}
class Circle : Shape, IEquatable<Circle>
{
public double Radius { get; set; }
public override bool Equals(Shape other) => Equals(other as Circle);
public bool Equals(Circle other)
=> other != null && Radius == other.Radius;
}
明示的実装
インターフェイスの場合、1つのクラスで複数のインターフェイスを実装することができます。 このとき、複数のインターフェイスに同名・同引数のメソッドがあった場合、衝突が起こりえます。
例えば以下の例を見てください。IAccumulator
インターフェイスとIGroup<T>
インターフェイスがどちらもAdd
メソッドを持っていて、それを両方実装しているImplicitImplementation
クラスは、1つのAdd
メソッドが2つの役割を兼ねることになります。
using System.Collections.Generic;
interface IAccumulator
{
void Add(int value);
int Sum { get; }
}
interface IGroup<T>
{
void Add(T item);
IEnumerable<T> Items { get; }
}
/// <summary>
/// 1つの<see cref="Add(int)"/>で、2つのインターフェイスの実装を担うんであれば特に問題は出ない。
/// </summary>
class ImplicitImplementation : IAccumulator, IGroup<int>
{
public void Add(int x)
{
Sum += x;
_items.Add(x);
}
public IEnumerable<int> Items => _items;
private List<int> _items = new List<int>();
public int Sum { get; private set; }
}
元々役割を兼ねたい場合はこれでいいんですが、そうでないこともあります。
こういう時に使うのが、インターフェイスの明示的実装です。
メンバーを定義する際に、メンバー名の前に「インターフェイス名 + .
」を加えます。
例えば、メソッドの場合は以下のように書きます。
戻り値の型 インターフェイス名.メソッド名(引数一覧)
{
メソッド本体(具体的な処理)
}
この場合、アクセス修飾子(public
やprivate
などは付けれません。)
これを使って、先ほどの2つのインターフェイスのAdd
メソッドに対して別実装を与えてみましょう。
以下のようになります。
/// <summary>
/// <see cref="IAccumulator.Add(int)"/>と、<see cref="IGroup{int}.Add(int)"/>が完全に被るので、
/// 別の実装を与えたければ明示的実装が必要。
/// </summary>
class ExplicitImplementation : IAccumulator, IGroup<int>
{
void IAccumulator.Add(int value) => Sum += value;
void IGroup<int>.Add(int item) => _items.Add(item);
public IEnumerable<int> Items => _items;
private List<int> _items = new List<int>();
public int Sum { get; private set; }
}
この例のように、明示的実装はメンバー単位で切り替えれます。
この例の場合は、Add
だけが明示的実装で、残りのSum
やItems
は通常の(暗黙的な)実装です。
ちなみに、明示的実装をしたメンバーは、そのクラスの変数から直接は利用できなくなります。 一度インターフェイスのキャストしてから呼び出すことになります。
using System;
class ExpliciteImplementationSample
{
public static void Main()
{
// 1つのAddで両方の債務を担ってるので2重集計される
var a = new ImplicitImplementation();
for (int i = 0; i < 5; i++)
{
Accumulate(a, i);
AddItem(a, i);
// 通常の実装なので、普通に Add(i) を呼ぶことも可能
//a.Add(i);
}
Console.WriteLine($"sum = {a.Sum}, items = {string.Join(", ", a.Items)}");
// 明示的実装を使って2つのAddを別実装したので個別集計される。
var b = new ExplicitImplementation();
for (int i = 0; i < 5; i++)
{
Accumulate(b, i);
AddItem(b, i);
// 明示的実装の場合、一度インターフェイスにキャストしてからでないと Add(i) は呼べない。
// 例えば以下のコメントを外すとコンパイル エラー。
//b.Add(i);
}
Console.WriteLine($"sum = {b.Sum}, items = {string.Join(", ", b.Items)}");
}
static void Accumulate(IAccumulator x, int value) => x.Add(value);
static void AddItem<T>(IGroup<T> g, T item) => g.Add(item);
}
まとめると、インターフェイスの明示的実装を使うと、以下のような状態になります。
- 同じ名前のメンバーを持ったインターフェイスを複数同時に実装できる
- 明示的実装したメンバーは、いったんインターフェイス型にキャストしてからでないと呼べなくなる