BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル C# 8のデフォルトインターフェースメソッド

C# 8のデフォルトインターフェースメソッド

原文(投稿日:2018/06/26)へのリンク

主要な論点

  • C# 8の新機能としてデフォルトインターフェースメソッドが提案されている。これはトレイトというプログラミングテクニックを可能にするものである。
  • トレイトとは、関連のないクラス間でメソッドを再利用できるオブジェクト指向プログラミング技術である。
  • C# 言語の開発チームは、Javaにおけるデフォルトメソッドという概念をベースとした。
  • C# は、デフォルトインターフェースメソッドで起こりうるダイヤモンド継承問題を、最も具体的なオーバーライドを実行時に選ぶことで解決する。
  • C# のコンパイラーは、デフォルトインターフェースメソッドを利用した時に、一般的な実装エラーを多く検出することで、誤りを防ごうとする。
 

デフォルトインターフェースメソッド(仮想拡張メソッドとも呼ばれる)とはC# 8の新機能として提案されているもので、これによりC# 開発者がトレイトと呼ばれるプログラミングテクニックを使えるようになる。トレイトとは、関連のないクラス間でメソッドを再利用できるオブジェクト指向プログラミング技術である。

本記事では、C# の新しい構文をはじめとする、この新しい機能の最も重要な点について説明し、この機能によってコードがより簡潔できれいなものになる例を示す。

デフォルトメソッドがもたらす主な利点は、既存のインターフェースに新しいデフォルトメソッドを追加しても、そのインターフェースを実装しているクラスを壊さずにすむことである。つまり、この機能では、当メソッドをオーバーライドするかどうかをクラスの実装者が決めることができる。

この機能にうってつけのシナリオの一つが、以下に説明するロギングの例である。ILoggerインターフェースが持つ抽象メソッドはWriteLogCoreただ一つだけで、WriteErrorやWriteInformationのような他のメソッドはすべて、それぞれ別の設定でWriteLogCoreメソッドを呼び出すデフォルトメソッドとして定義されている。ILoggerの実装者は、WriteLogCoreメソッドだけを実装すればよい。

ロガー型の各実装クラスで削減されるコードの行数を考えてもらいたい。この機能は素晴らしいものにもなり得るが、危険性がないわけではない。一種の多重継承であるので、ダイヤモンド問題が起こり得る。これについては後で述べる。また、インターフェースメソッドは、状態を持たない「純粋な振る舞い」でなければならない。つまり、インターフェースは、これまで同様、フィールドを直接参照することができない。

.NETコンパイラーでは、以下に挙げたインターフェースの新しいキーワードを受け入れるように、C# のインターフェース構文が拡張されている。たとえば、インターフェースにプライベートメソッドを書いたとしても、コンパイルは通り、動作する。

  • メソッド、インデクサー、イベントアクセサーの本体
  • private, protected, internal, public, virtual, abstract, override, sealed, static, extern
  • 静的フィールド
  • 静的なメソッド、プロパティ、インデクサー、イベント
  • 明示的なアクセス修飾子(既定のアクセスレベルはpublic)
  • override修飾子

一方、以下は利用できない。

  • インスタンスの状態、インスタンスフィールド、インスタンスの自動プロパティ

デフォルトインスタンスメソッドの例

次の単純な例をもとにこの機能の動作を説明する。

// ------------------------デフォルトインスタンスメソッド---------------------------

  interface IDefaultInterfaceMethod
  {
    public void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }
  }

  class AnyClass : IDefaultInterfaceMethod
  {
  }

  class Program
  {
    static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();
    }
  }

コンソールの出力:

> I am a default method in the interface!

上のコードを見ると、インターフェースがデフォルトメソッドを持ち、実装クラスにはこのデフォルトメソッドの知識も含まれていなければ、メソッドの実装も含まれていないことがわかる。

IDefaultInterfaceMethodAnyClassに変更したものが以下である。

AnyClass anyClass = new AnyClass();
  anyClass.DefaultMethod();

上のコードは「AnyClassにはDefaultMethodの定義が含まれていない」というコンパイル時エラーとなる。

この結果が、実装クラスはデフォルトメソッドのことを全く知らないという証拠になる。

[クリックで画像が拡大]

図 -1- クラスに対する呼び出しのエラーメッセージ

インターフェースが持つデフォルトメソッドにアクセスするには、オブジェクトをインターフェース型にアップキャストしなければならない。

AnyClass anyClass = new AnyClass();
  ((IDefaultInterfaceMethod)anyClass).DefaultMethod(); 

コンソールの出力:

> I am a default method in the interface!

言及しておかなかればならないのは、Javaには前から同じ機能が存在しており、.NETチームはJavaのデフォルトメソッドのドキュメントを.NET Framework開発者のリファレンスとみなしていることだ。以下に例を示す。

“我々はJavaではどうなっているかを深く理解しておく必要がある。このトピックについての洞察がすでに蓄積されているはずである。” - 2017年4月11日のC# 言語設計ノート

インターフェース内の修飾子

すでに述べた通り、インターフェースのC# 構文は protected、internal、public、virtualというキーワードを受け入れるように拡張されている。既定では、sealedまたはprivateで修飾されていない限り、デフォルトインターフェースメソッドは仮想メソッドである。同様に、本体を持たないインターフェースメンバーの場合は、既定はabstractである。

例:

  // ------------------------ 仮想と抽象 ---------------------------

interface IDefaultInterfaceMethod
  {
    // 既定では、このメソッドはvirtualである。virtualキーワードを明示的に使うこともできる。
    virtual void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }

    // 既定では、このメソッドはabstractである。abstractキーワードを明示的に使うこともできる。
    abstract void Sum();
  }

  interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method!");
    }
  }

  class AnyClass : IDefaultInterfaceMethod, IOverrideDefaultInterfaceMethod
  {
    public void Sum()
    {
    }
  }

  class Program
  {
      static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();

      IOverrideDefaultInterfaceMethod anyClassOverridden = new AnyClass();
      anyClassOverridden.DefaultMethod();
    }
  }

コンソールの出力:

> I am a default method in the interface!
> I am an overridden default method!

上のインターフェースからvirtualおよびabstractキーワードを削除することもできるが、コンパイル結果のコードには何も影響しない。

注意: オーバーライドしているメソッドにはアクセス修飾子を付与できない。

オーバーライドの例:

interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    public void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method");
    }
  }

上のコードは「修飾子 ‘public’ がこの項目に対して有効ではありません。」というコンパイル時エラーとなる。

[クリックで画像が拡大]

図 -2- オーバーライドしているメソッドには修飾子が許されない

ダイヤモンド問題

多重継承を許可することであいまいさが発生する可能性がある。これはC++のように状態の多重継承が可能な言語では大きな問題である。ただし、C# ではクラスに対する多重継承は許可されておらず、インターフェースに対してだけで、しかも方法は制限されているため、状態は含まれない。

図 -3- ダイヤモンド問題の依存関係

以下の状況を考えてみる。

// ------------------------ダイヤモンド継承とクラス---------------------------

  interface A
  {
    void m();
  }

  interface B : A
  {
    void A.m() { System.Console.WriteLine("interface B"); }
  }

  interface C : A
  {
    void A.m() { System.Console.WriteLine("interface C"); }
  }

  class D : B, C
  {
    static void Main()
    {
      C c = new D();
      c.m();
    }
  }

上のコードは、以下の図 -4- に示すコンパイル時エラーが発生する。

[クリックで画像が拡大]

図 -4- ダイヤモンド問題のエラーメッセージ

.NET開発チームは、実行時に最も具体的なオーバーライドを選択することでダイヤモンド問題を解決する方法を選んだ。

"ダイヤモンド継承とクラス
クラスによるインターフェースメンバーの実装は、基底クラスから継承されたものであっても、インターフェースのデフォルト実装より常に優先されなければならない。デフォルト実装は常に、クラスがそのメンバーの実装をまったく持たない場合にのみ、フォールバックとなる。"

この問題について詳しく知りたい場合は、提案: デフォルトインターフェースメソッド および 2017年4月19日のC# 言語設計ノートを参照のこと。

上記の例に戻ると、問題は最も具体的なオーバーライドをコンパイラーが推論できないことである。ただし、クラス ‘D’ に以下のような ‘m’ メソッドを追加すれば、コンパイラーはクラスによる実装を使用してダイヤモンド問題を解決できる。

  class D : B, C
  {
    // こうすることで、コンパイラーはクラス 'D' に定義された実装が最も具体的なものと判断する
    void A.m()
    {
       System.Console.WriteLine("I am in class D"); 
    }

    static void Main()
    {
      A a = new D();
      a.m();
    }
  }

コンソールの出力:

> I am in class D


thisキーワード
以下のコードは、インターフェース内で‘this’キーワードを使用する例である。

public interface IDefaultInterfaceWithThis
  {
    internal int this[int x]
    {
      get
      {
        System.Console.WriteLine(x);
        return x;
      }
      set
      {
        System.Console.WriteLine("SetX");
      }
    }

    void CallDefaultThis(int x)
    {
      this[0] = x;
    }
  }

  class DefaultMethodWithThis : IDefaultInterfaceWithThis
  {
  }

コードの使用例:

  IDefaultInterfaceWithThis defaultMethodWithThis = new DefaultMethodWithThis();
  Console.WriteLine(defaultMethodWithThis[0]);  
  defaultMethodWithThis.CallDefaultThis(0); 

コンソールの出力:

0
SetX

例: ILogger

ロガーインターフェースは、デフォルトメソッドのテクニックを説明する際に最もよく使われる例である。下のコード例では、"WriteCore"という名前の抽象メソッドが1つ含まれている。他のメソッドにはデフォルトの実装がある。ConsoleLoggerTraceLoggerがインターフェースを実装している。下のコードを見れば、コードがきれいで簡潔であることがわかるだろう。これまでは、インターフェースを実装するクラスには、クラスを抽象クラスにしない限りメソッドを実装しなければならなかったため、コードをDRYにするために抽象クラスが使われることもあった。新しいアプローチでは、ConsoleLoggerクラスは別のクラス階層から継承することができる。つまり、デフォルトメソッドによって、最も柔軟性の高い設計にすることができるようになる。

  enum LogLevel
  {
    Information,
    Warning,
    Error
  }

  interface ILogger
  {
    void WriteCore(LogLevel level, string message);

    void WriteInformation(string message)
    {
      WriteCore(LogLevel.Information, message);
    }

    void WriteWarning(string message)
    {
      WriteCore(LogLevel.Warning, message);
    }

    void WriteError(string message)
    {
      WriteCore(LogLevel.Error, message);
    }
  }

  class ConsoleLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      Console.WriteLine($"{level}: {message}");
    }
  }

  class TraceLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      switch (level)
      {
        case LogLevel.Information:
          Trace.TraceInformation(message);
          break;

        case LogLevel.Warning:
          Trace.TraceWarning(message);
          break;

        case LogLevel.Error:
          Trace.TraceError(message);
          break;
      }
    }
  }

コードの使用例:

      ILogger consoleLogger = new ConsoleLogger();
      consoleLogger.WriteWarning("Cool no code duplication!");  // Output: Warning: Cool no Code duplication!

      ILogger traceLogger = new TraceLogger();
      consoleLogger.WriteInformation("Cool no code duplication!");  // Cool no Code duplication!

例: Player

さまざまな種類のプレーヤーを含むゲームを考える。パワープレイヤーがダメージを最も多く与える一方で、制限プレイヤーは与えるダメージが少ない。

  public interface IPlayer
  {
    int Attack(int amount);
  }

  public interface IPowerPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 50;
    }
  }

  public interface ILimitedPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 10;
    }
  }

  public class WeakPlayer : ILimitedPlayer
  {
  }

  public class StrongPlayer : IPowerPlayer
  {
  }

コードの使用例:

  IPlayer powerPlayer = new StrongPlayer();         
  Console.WriteLine(powerPlayer.Attack(5));  // Output 55

  IPlayer limitedPlayer = new WakePlayer();
  Console.WriteLine(limitedPlayer.Attack(5));  // Output 15

上のコード例で示した通り、IPowerPlayer インターフェースとILimitedPlayer インターフェースにデフォルト実装がある。制限プレイヤーは与えるダメージが少ない。StrongPlayerクラスを継承した新しいクラス(たとえば、SuperDuperPlayer)を定義した場合、新しいクラスは、次の例に示すように、デフォルトのパワー攻撃の振る舞いをIPowerPlayer インターフェースから自動的に受け継ぐ。

  public class SuperDuperPlayer: StrongPlayer
  {
  }

  IPlayer superDuperPlayer = new SuperDuperPlayer();
  Console.WriteLine(superDuperPlayer.Attack(5)); // 出力は 55

例: ジェネリックなフィルター

ApplyFilterは、ジェネリックな型に述語関数を適用するデフォルトインターフェースメソッドである。この例では、モックの振る舞いをシミュレートするためにダミーのフィルターを用いている。

  interface IGenericFilter<T>
  {
    IEnumerable<T> ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      foreach (var item in collection)
      {
        if (predicate(item))
        {
          yield return item;
        }
      }
    }
  }

  interface IDummyFilter<T> : IGenericFilter<T>
  {
    IEnumerable<T> IGenericFilter<T>.ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      return default;
    }
  }

  public class GenericFilterExample: IGenericFilter<int>, IDummyFilter<int>
  {
  }

コードの使用例:

      IGenericFilter<int> genericFilter = new GenericFilterExample();
      var result = genericFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

コンソールの出力:

2, 3
      IDummyFilter<int> dummyFilter = new GenericFilterExample();
      var emptyResult = dummyFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

コンソールの出力:

0

このようなジェネリックフィルターの概念は、他にも多くの設計や要件に適用できる。

制限事項

インターフェース内で修飾子を使用するときに理解しておくべき制限事項と考慮事項がいくつかある。多くの場合、コンパイラーは、以下に挙げるような一般的なエラーを検出し、誤りを防ぐ。

以下のコードを考える。

  interface IAbstractInterface
  {
    abstract void M1() { }
    abstract private void M2() { }
    abstract static void M3() { }
    static extern void M4() { }
  }

  class TestMe : IAbstractInterface
  {
    void IAbstractInterface.M1() { }
    void IAbstractInterface.M2() { }
    void IAbstractInterface.M3() { }
    void IAbstractInterface.M4() { }
  }

上のコードは以下に挙げるコンパイル時エラーとなる。

エラー CS0500: 'IAbstractInterface.M1()' は abstract に指定されているため本体を宣言できません。
エラー CS0621: 'IAbstractInterface.M2()': 仮想または抽象メンバーには、private を指定できません。
エラー CS0112: 静的メンバー 'IAbstractInterface.M3()' を override、virtual、または abstract とすることはできません。
エラー CS0179: 'IAbstractInterface.M4()' を extern にして、本体を宣言することはできません。
エラー CS0122: 'IAbstractInterface.M2()' はアクセスできない保護レベルになっています。

エラー CS0500が示すのは、デフォルトメソッド 'IAbstractInterface.M3()' は抽象メソッドでありながら本体を持つことはできないということである。エラー CS0621は、当該メソッドはプライベートでありながら抽象メソッドにはできないということである。

Visual Studioでは以下の通り:

[クリックで画像が拡大]

図 -5- Visual Studioでのコンパイルエラー

ソースコードリポジトリおよびその他の開発者向けリソース

詳細情報とソースコード:

この記事の著者

Bassam Alugiliは、STRATEC AGのシニアソフトウェアスペシャリストであり、データベースの専門家である。STRATECは、完全自動分析システム、ラボデータ管理ソフトウェア、スマート消費財で世界をリードするパートナーである。

 

この記事に星をつける

おすすめ度
スタイル

BT