複数のインターフェイスを実装

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名前空間)について、異なる型引数で複数実装できます。 ABという2つのクラスがあったとして、IEquatable<A>IEquatable<B>という2つの実装を持てます。

具体的な用途としては、例えば、以下のような場面で有効です。

  • 図形全般を表すShape型がある
  • Shapeから派生した、矩形型Rectangleがある
    • Rectangleは、幅と高さの両方の比較で等値判定する
  • Shapeから派生した、円型Circleがある
    • Circleは、半径の比較で等値判定する
  • Shapeは、矩形同士、円同士でだけ等値判定をする。型が違う場合はその時点で不一致

この条件下では、それぞれのクラスに以下のようにインターフェイスを持てます。

  • Shapeは他のShapeと比較できるので、IEquatable<Shape>を実装できる
  • Rectangleは他のRectangleと比較できるので、IEquatable<Rectangle>を実装できる
    • RectangleShapeから派生しているので、IEquatable<Shape>でもある
  • Circleは他のCircleと比較できるので、IEquatable<Circle>を実装できる
    • CircleShapeから派生しているので、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; }
}

元々役割を兼ねたい場合はこれでいいんですが、そうでないこともあります。 こういう時に使うのが、インターフェイスの明示的実装です。 メンバーを定義する際に、メンバー名の前に「インターフェイス名 + .」を加えます。 例えば、メソッドの場合は以下のように書きます。

戻り値の型 インターフェイス名.メソッド名(引数一覧)
{
    メソッド本体(具体的な処理)
}

この場合、アクセス修飾子(publicprivateなどは付けれません。)

これを使って、先ほどの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だけが明示的実装で、残りのSumItemsは通常の(暗黙的な)実装です。

ちなみに、明示的実装をしたメンバーは、そのクラスの変数から直接は利用できなくなります。 一度インターフェイスのキャストしてから呼び出すことになります。

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);
}

まとめると、インターフェイスの明示的実装を使うと、以下のような状態になります。

  • 同じ名前のメンバーを持ったインターフェイスを複数同時に実装できる
  • 明示的実装したメンバーは、いったんインターフェイス型にキャストしてからでないと呼べなくなる

更新履歴

ブログ