C Sharpの基礎 - インターフェイス

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

インターフェースとは、クラスが実装すべき規約を定めるものである。
インターフェイスには、非抽象クラスまたは構造体で実装する必要がある関連する機能のグループに対する定義が含まれる。

特に、C#はクラスの多重継承ができないため、インターフェイスは重要である。

また、C# 8.0以降では、メンバの既定の実装を定義できる。
ただし、インターフェイスでは、フィールド、自動実装プロパティ、プロパティに似たイベント等は宣言できない。

構造体を継承する場合は、インターフェイスを使用する必要がある。
C#の構造体は、別の構造体またはクラスから継承することができないからである。

以下に、インターフェイスの特徴を示す。

  • publicの抽象メソッドのみを持つクラスのようなもの。
  • インターフェイスを定義する時は、interfaceキーワードを使用する。
  • 抽象クラスとは異なり、複数のインターフェースを継承できる。
  • C# 8.0以降では、メンバの既定の実装を定義できる。



インターフェース

インターフェースとは、規約のみを定めるものである。

C#では、抽象メソッドを使用することでメソッドの規約のみを定めることができる。
つまり、C#のインターフェースとは、抽象メソッドのみを持つ抽象クラスだと考えることができる。

C#のインターフェースの定義は以下のように記述する。

 interface インタフェス名
 {
    // メソッド・プロパティの宣言
 }


インターフェースの実装は、クラスの継承と同じ構文である。

 class クラス名 : インタフェス名
 {
    // クラスの定義
 }


インターフェースには、以下のような特徴がある。

  • メンバ変数(フィールド)を持つことができない。
  • staticメソッドを持つことができない。
  • 宣言したメソッドおよびプロパティは、全てpublic abstractになる。
  • 1つのクラスが複数のインターフェースを実装(多重継承)できる。


C# 8.0では、制限がいくつか緩和されている。
機能面でいうと、クラスと抽象クラスとの差は、メンバ変数(フィールド)を持てない代わりに多重継承できる程度である。


標準クラスライブラリのインターフェース

.NET Frameworkの標準クラスライブラリでは、いくつかの汎用性の高いインターフェースが標準で存在する。
以下では、そのうちのいくつかを記載する。

IComparable

IComparable<T>インターフェイス(System名前空間)は、順序比較ができるものを表す。
配列の整列等に使用する。

 using System;
 using System.Linq;
 
 /// <summary>
 /// 2次元上の点
 /// <see cref="IComparable{T}"/> を実装している = 順序をつけられる。
 /// </summary>
 class Point2D : IComparable<Point2D>
 {
    public double X { get; }
    public double Y { get; }
 
    public Point2D(double x, double y)
    {
       X = x;
       Y = y;
    }
 
    public double Radius => Math.Sqrt(X * X + Y * Y);
    public double Angle => Math.Atan2(Y, X);
 
    /// <summary>
    /// 距離で順序を決める。
    /// 距離が全く同じなら偏角で順序付け。
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public int CompareTo(Point2D other)
    {
       var r = Radius.CompareTo(other.Radius);
       if (r != 0)
       {
          return r;
       }
 
       return Angle.CompareTo(other.Angle);
    }
 }
 
 class IComparableSample
 {
    public static void Main()
    {
       const int N = 5;
       var rand = new Random();
       var data = Enumerable.Range(0, N).Select(_ => new Point2D(rand.NextDouble(), rand.NextDouble())).ToArray();
 
       Console.WriteLine("元:");
       foreach (var p in data) WriteLine(p);
 
       // 並べ替えの順序に使える
       Console.WriteLine("整列済み:");
       foreach (var p in data.OrderBy(x => x)) WriteLine(p);
    }
 
    private static void WriteLine(Point2D p)
    {
       Console.WriteLine($"({p.X:N3}, {p.Y:N3}), radius = {p.Radius:N3}, angle = {p.Angle:N3}");
    }
 }


コレクション

コレクションには、同じ操作ができる様々な実装方法がある。(それぞれ、メリットおよびデメリットがある)
C#では、操作の種類ごとにインターフェイスが標準で存在しており、コレクションはそれらのインターフェイスを実装する。

下表に、その例を示す。(いずれもSystem.Collections.Generic名前空間)

インターフェイス 説明
IEnumerable<T> 要素の列挙ができる。
foreach文やLINQ to Objectsで使用する。
ICollection<T> IEnumerable<T>に加えて、要素の追加(Add)、削除(Remove)、要素の個数の取得ができる。
IList<T> ICollection<T>に加えて、インデクサを使用した要素の読み書きができる。
IDictionary<TKey, TValue> 辞書アクセス(キーを使った値の検索)しての値の読み書きができる。
IReadOnlyCollection<T>
.NET Framework 4.5以降とC# 5.0以降
IEnumerable<T>に加えて、要素の個数を取得できる。
読み取り専用なので共変。
IReadOnlyList<T>
.NET Framework 4.5以降とC# 5.0以降
IReadOnlyCollection<T>に加えて、インデクサを使用した要素の読み取りができる。
読み取り専用なので共変。
IReadOnlyDictionary<TKey, TValue>
.NET Framework 4.5以降とC# 5.0以降
辞書アクセス(キーを使った値の検索)しての値の読み取りができる。


上表のうち、IEnumerable<T>IReadIReadOnlyList<T>の例を示す。

 using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 
 /// <summary>
 /// 連結リスト
 /// <see cref="IEnumerable{T}"/> を実装している = データの列挙ができる。複数のデータを束ねてる。
 /// </summary>
 /// <typeparam name="T"></typeparam>
 class LinkedList<T> : IEnumerable<T>
 {
    public T Value { get; }
    public LinkedList<T> Next { get; }
 
    public LinkedList(T value) : this(value, null) { }
    private LinkedList(T value, LinkedList<T> next) { Value = value; Next = next; }
 
    public LinkedList<T> Add(T value) => new LinkedList<T>(value, this);
 
    public IEnumerator<T> GetEnumerator()
    {
       if(Next != null)
       {
          foreach (var x in Next)
          {
             yield return x;
          }
 
          yield return Value;
       }
    }
 
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 }
 
 class IEnumerableSample
 {
    public static void Main()
    {
       var a = new LinkedList<int>(1);
       var b = a.Add(2).Add(3).Add(4);
 
       // foreach で使える(これは IEnumerable 必須ではない)
       foreach (var x in b)
          Console.WriteLine(x);
 
       // string.Join で使える
       Console.WriteLine(string.Join(", ", b));
 
       // LINQ で使える
       Console.WriteLine(b.Sum());
    }
 }


 using System;
 using System.Collections;
 using System.Collections.Generic;
 
 /// <summary>
 /// 4次元上の点
 /// <see cref="IReadOnlyList{T}"/> を実装している = <see cref="IEnumerable{T}"/>に加えて、インデックス指定で値を読める。
 /// </summary>
 class Point4D : IReadOnlyList<double>
 {
    public double X { get; }
    public double Y { get; }
    public double Z { get; }
    public double W { get; }
 
    public Point4D(double x, double y, double z, double w)
    {
       X = x; Y = y; Z = z; W = w;
    }
 
    public double this[int index]
    {
       get
       {
          switch (index)
          {
             default:
             case 0: return X;
             case 1: return Y;
             case 2: return Z;
             case 3: return W;
          }
       }
    }
 
    public int Count => 4;
 
    public IEnumerator<double> GetEnumerator()
    {
       yield return X;
       yield return Y;
       yield return Z;
       yield return W;
    }
 
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 }
 
 class IReadOnlyListSample
 {
    public static void Main()
    {
       var p1 = new Point4D(1, 2, 3, 4);
       var p2 = new Point4D(3, 7, 5, 11);
 
       // X, Y, Z, W の代わりに 0, 1, 2, 3 のインデックスで値を読み出し
       var innerProduct = 0.0;
       for (int i = 0; i < 4; i++)
       {
          innerProduct += p1[i] * p2[i];
       }
 
       Console.WriteLine(innerProduct);
    }
 }


IDisposable

IDisposableインターフェイス(System名前空間)は、明示的なタイミングで破棄処理を行う場合に使用する。

 using System;
 
 /// <summary>
 /// <see cref="IDisposable"/> を実装している = 使い終わったら明示的に Dispose を呼ぶ必要がある。
 /// </summary>
 class Stopwatch : IDisposable
 {
    System.Diagnostics.Stopwatch _s = new System.Diagnostics.Stopwatch();
 
    public Stopwatch() { _s.Start(); }
 
    public void Dispose()
    {
       _s.Stop();
       Console.WriteLine(_s.Elapsed);
    }
 }
 
 class IDisposableSample
 {
    public static void Main()
    {
       // using ブロックを抜けたら自動的に Dispose が呼ばれる
       using (new Stopwatch())
       {
          var t = T(12, 6, 0);
       }
    }
 
    private static int T(int x, int y, int z) => x <= y ? y : T(T(x - 1, y, z), T(y - 1, z, x), T(z - 1, x, y));
 }



複数のインターフェイスの実装(多重継承)

C#は、クラスの多重継承ができないが、インターフェイスの複数の実装ができる。

 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つのクラスで複数のインターフェイスを実装することができる。
この時、複数のインターフェイスに同名・同引数のメソッドが存在する場合、衝突が起きる。
そのため、衝突を回避するためには、インターフェイスの明示的実装を行う。

メンバを定義する時、メンバ名の前にインターフェイス名 + .を記述する。
例えば、メソッドの場合は、以下のように記述する。この時、アクセス修飾子(publicprivate等は指定できない)

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


明示的実装は、メンバ単位で切り替えることができる。
以下の例では、Addメソッドのみが明示的実装であり、SumメソッドやItemsメソッドは通常の(暗黙的な)実装である。

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


また,明示的実装を記述したメンバは、そのクラスの変数から直接使用できないため、
インターフェイスのキャストしてから呼び出すことになる。

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

  • 同名のメンバを持つインターフェイスを複数実装できる。
  • 明示的実装したメンバは、インターフェイス型にキャストしてから使用する。
 using System;
 
 class Sample
 {
    static void Accumulate(IAccumulator x, int value) => x.Add(value);
 
    static void AddItem<T>(IGroup<T> g, T item) => g.Add(item);
 
    public static void Main()
    {
       // 明示的実装を記述して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)}");
    }
 }


以下に、インターフェイスの明示的実装の用途を示す。

メソッドの隠蔽

明示的実装の性質を使用して、使用されない、または、使用されたくないメンバを隠蔽することができる。

.NET Frameworkの標準ライブラリには、隠蔽したいメソッドの例として、以下のようなものが存在する。

  • 非ジェネリックのIEnumerableインターフェイス(System.Collections名前空間)
    ジェネリックのIEnumerable<T>(System.Collections.Generic名前空間)は、この非ジェネリックから派生している。
  • ICollection<T>インターフェイス(System.Collections.Generic名前空間)のIsReadOnlyメソッド


以下の例では、IEnumerableインターフェイスを隠蔽している。

 using System.Collections;
 using System.Collections.Generic;
 
 class LinkedList<T> : IEnumerable<T>
 {
    public LinkedList(T value) : this(value, null)
    { }
 
    private LinkedList(T value, LinkedList<T> next)
    { Value = value; Next = next; }
 
    public T Value { get; }
    public LinkedList<T> Next { get; }
 
    public LinkedList<T> Add(T value) => new LinkedList<T>(value, this);
 
    public IEnumerator<T> GetEnumerator()
    {
       if(Next != null)
       {
          foreach (var x in Next)
          {
             yield return x;
          }
        }
        yield return Value;
    }
 
    // 明示的実装
    // IEnumerableを介さない限り見えない
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 }


※注意
インターフェイスの増加はコストが掛かるため、.NET Frameworkの初期では、インターフェイスを減少させる方向で設計を進めていた。
しかし現在では、"読み取り専用なコレクション"と"書き換え可能なコレクション"を別インターフェイスに分けるべきとの設計方針である。

メンバのアクセスの制限

  • internal setを隠す
  • internal interfaceとの組み合わせ


オブジェクトとジェネリック

特定のインターフェイスを実装している時だけ特別な動作をする、という処理を記述する場合がある。

  • as判定用に、interface IX { object X { get; } }
  • 手動で使用するためにジェネリックも記述して、interface IX<T> : IX { new T X { get; } }



インターフェイスの標準実装

C# 8.0(.NET Core 3.0)では、以下に示すように、インターフェイスの制限が緩和された。

  • メソッド、プロパティ、インデクサ、イベントのアクセサの実装を持つことができる。
  • アクセシビリティを明示的に指定できる。
  • 静的メンバを持つことができる。(入れ子の型も含む)


機能面では、クラス(特に、抽象クラス)との差は、フィールドを持つことができない代わりに多重継承できる程度である。
フィールドは、多重継承(特に、ひし形継承)との相性が悪くメリットが少ないため、これ以上の制限は緩和されない。

以下に、クラスと異なる部分を示す。

  • アクセシビリティ未指定の場合等、既定の挙動が異なる。
  • 派生インターフェイスでのオーバーライドは明示的実装が必須。
  • 標準実装を持つメンバは、派生クラスまたは派生インターフェイスから直接呼べない。(親へのキャストが必要)


C# 8.0(.NET Core 3.0)以降、インターフェイスは実装を持つことができる。

 interface I
 {
    void X();
 
    void Y()  // 標準実装を持っているため、コンパイル可能
    { }
 }
 
 class A : I
 {
    public void X()  // Xメソッドのみ実装する
    { }
 }
 
 class B : I
 {
    public void X() { }
 
    public void Y() => Console.WriteLine("B");  // Yメソッドも実装できる
 }


ただし、以下の制限は存在する。

  • フィールドを持つことはできない。(静的フィールドは可能)
  • コンストラクタおよびデストラクタは持つことができない。(静的コンストラクタは可能)


以下に、C# 8.0(.NET Core 3.0)以降、インターフェイスで緩和された項目を示す。

静的メンバ

静的メンバ、静的コンストラクタ、静的フィールドを持つことができる。
また、定数、演算子、入れ子の型も持つことができる。

アクセス修飾子は、protected、privateを指定することができる。指定しない場合、publicとなる。

 using System;
 
 interface I
 {
    static I()
    { }
 
    static int _field;
 
    static int Method() => ++_field;
 
    const int Constant = 1;
 
    public static I operator +(I x) => x;
 
    class Inner { }
 }
 
 class Program
 {
    static void Main()
    {
       Console.WriteLine(I.Method());
       I.Inner inner;
    }
 }


アクセス修飾子

C# 7.3以前は、インターフェイスのメンバは常にvirtualかつpublicであった。
C# 8.0以降、明示的に指定することで、protected、private等のアクセス修飾子を指定できる。

protectedメンバにアクセスできるのは、派生インターフェイスのみである。
インターフェイスを実装(派生)しているクラスの場合、protectedメンバにはアクセスできない。

 interface I
 {
    void Public()
    {
        Private();
    }
 
    // アクセス修飾子を指定できる
    // protectedの場合、派生インターフェイスからのみ呼び出すことができる
    // privateの場合、自身からのみ呼び出すことができる
    protected void Protected() { }
    private void Private() { }
 }
 
 interface IDerived : I
 {
    void M()
    {
       Public();
       Protected();
       // Private();
    }
 }


既定で仮想

protectedやinternal等を付加する場合、自動的にprotected virtualやinternal virtualとなる。
privateやsealedを指定する場合のみ、非仮想となる。

 interface I
 {
    // 未指定の場合、public virtual
    void Public()
    { }
 
    // protectedを指定する場合、protected virtual
    protected void Protected()
    { }
 
    // privateメンバは派生側から呼ばれないため、virtualである必要がない
    private void Private()
    { }
 
    // sealedを指定する場合、非virtualとなる
    sealed void Sealed()
    { }
 }


また、基底インターフェイスのvirtualメンバは、派生インターフェイスでsealedに変更することができない。
virtualメンバは、常にvirtualとなる。

 interface IDerived : I
 {
    // 基底インターフェイスのvirtualメンバは、派生インターフェイスでsealedに変更することができない
    // コンパイルエラー
    sealed void I.Protected()
    { }
 }


多重継承

例えば、以下のような記述では、どの実装を使用すべきか不明瞭であるため、コンパイルエラーが起きる。

 using System;
 
 interface IA
 {
    void M() => Console.WriteLine("A.M");
 }
 
 interface IB : IA
 {
    void IA.M() => Console.WriteLine("B.M");
 }
 
 interface IC : IA
 {
    void IA.M() => Console.WriteLine("C.M");
 }
 
 // IBおよびICにMメソッドの実装があるため、どちらを使用すべきか不明瞭である(コンパイルエラー)
 class C : IB, IC
 {
    // ...略
 }


ただし、クラス自身が実装を持つ場合はクラスのメソッドが優先されるため、コンパイルエラーは起きない。

 class C : IB, IC
 {
    // IB.MまたはIC.Mではなく、このMが呼ばれるためコンパイル可能
    public void M() => Console.WriteLine("new implementation");
 }


C# 9.0では、特定の実装を呼ぶ場合、baseキーワードに特定の型を指定できる機能が追加される。
baseキーワードは、クラスでも使用できる。

 class C : IB, IC
 {
    // baseキーワードを使用することで、明示的にIB.Mメソッドを呼ぶことができる
    public void M() => base(IB).M();
 }


再抽象化

標準実装を持つメンバを、再び、派生インターフェイス側で抽象メンバに戻すこともできる。
明示的実装のような記述に、abstract修飾を付加する。

以下の例では、Mメソッドが抽象メンバであるため、インターフェイスBを実装するクラスにはMメソッドの実装が必須となる。
この機能を、再抽象化(re-abstraction)と呼ぶ。

 using System;
 
 interface A
 {
    void M() => Console.WriteLine("default implementation");
 }
 
 interface B : A
 {
    // 実装を持っているメソッドをabstractに変更する
    abstract void A.M();
 }
 
 // Mメソッドの実装が必須となるため、コンパイルエラーが起きる
 class C : B
 {
 }


その他の制限

既存(C# 7.3以前)の破壊的変更が起きないようにするためであるが、その他、いくつか制限が存在する。
派生クラスと派生インターフェイスで挙動が変わることもあるため、注意が必要である。

まず、派生インターフェイスでは、オーバーライドは常に明示的実装が必要である。

 interface I
 {
    void M() { }
 }
 
 interface IDerived : I
 {
    // オーバーライドには明示的実装が必須
    void I.M()
    { }
 
    // 単にMメソッドを記述すると、別メソッドとなる
    // "別メソッドで基底のMメソッドを隠蔽する場合は、newを付加する"ように警告が出力される
    void M()
    { }
 }
 
 class C : I
 {
    // クラスの場合はそのような制限は無く、publicの同名のメソッドを記述すればI.Mとして使用できる
    public void M()
    { }
 }


派生インターフェイスから基底インターフェイスのメンバを呼び出す場合は、クラスと同様である。
一方、派生クラスからインターフェイスのメンバを呼び出す場合、標準実装のみ(オーバーライドしない)の時は、メンバを直接呼ぶことができない。

また、protected修飾子のものを呼ぶこともできない。

 interface I
 {
    void Abstract();
    void Default()
    { }
 
    protected void Protected()
    { }
 }
 
 interface IDerived : I
 {
    void M()
    {
       // 派生クラスから基底クラスの呼び出しと同様
       // public, protected, 標準実装の有無も関係なく呼ぶことができる
       Abstract();
       Default();
       Protected();
    }
 }
 
 class C : I
 {
    // 標準実装が無いものは実装が必須
    public void Abstract()
    { }
 
    public void M()
    {
       // 自身も実装を持つため呼ぶことができる。
       Abstract();
 
       // コンパイルエラー
       // インターフェイスの標準実装は直接呼ぶことができない
       Default();
 
       // インターフェイスの標準実装を呼ぶ場合は、キャストが必要となる
       ((I)this).Default();
 
       // コンパイルエラー
       // protectedを付加したものは呼ぶことができない
       ((I)this).Protected();
    }
 }