チュートリアル: 既定のインターフェイス メソッドでインターフェイスを使用してクラスを作成するときの機能の混合

インターフェイスのメンバーを宣言するときに実装を定義できます。 この機能により、インターフェイスで宣言された機能の既定の実装を定義できるという新機能が提供されます。 クラスでは、機能をオーバーライドする場合、既定の機能を使用する場合、および個別の機能のサポートを宣言しない場合を選択できます。

このチュートリアルでは、次の作業を行う方法について説明します。

  • 個別の機能を記述する実装を備えたインターフェイスを作成します。
  • 既定の実装を使用するクラスを作成します。
  • 既定の実装の一部またはすべてをオーバーライドするクラスを作成します。

必須コンポーネント

C# コンパイラを含め、.NET が実行されるようにコンピューターを設定する必要があります。 C# コンパイラは、Visual Studio 2022、または .NET SDK で使用できます。

拡張メソッドの制限事項

インターフェイスの一部として表示される動作を実装する方法の 1 つとして、既定の動作を提供する拡張メソッドを定義することがあります。 インターフェイスでは、そのインターフェイスを実装するクラスに対してより広い範囲の外部からのアクセスを提供すると同時に、最小セットのメンバーを宣言します。 たとえば、Enumerable の拡張メソッドは、任意のシーケンスの実装を LINQ クエリのソースとして提供します。

拡張メソッドは、コンパイル時に変数の宣言型を使用して解決されます。 インターフェイスを実装するクラスを使用すると、すべての拡張メソッドに対してより適切な実装を提供できます。 コンパイラで実装を選択できるようにするには、変数宣言が実装する型と一致している必要があります。 コンパイル時の型がインターフェイスと一致する場合、メソッドの呼び出しは拡張メソッドに解決されます。 拡張メソッドに関するもう 1 つの問題は、拡張メソッドを含むクラスにアクセスできる場所であれば、これらのメソッドにアクセスできることです。 クラスでは、拡張メソッドで宣言された機能を提供する必要があるかどうかを宣言できません。

既定の実装をインターフェイス メソッドとして宣言できます。 その後は、すべてのクラスで自動的に既定の実装が使用されます。 より適切な実装を提供できるクラスでは、インターフェイス メソッドの定義をより適切なアルゴリズムでオーバーライドできます。 ある意味では、この手法は拡張メソッドを使用する方法と似ています。

この記事では、既定のインターフェイスの実装で新しいシナリオを実現する方法について説明します。

アプリケーションを設計する

ホーム オートメーション アプリケーションについて考えてみましょう。 家庭内で使用される可能性がある照明とインジケーターには、おそらくさまざまな種類があります。 すべての照明が、オン/オフを切り替え、現在の状態を報告する API をサポートする必要があります。 一部の照明とインジケーターでは、次のような他の機能がサポートされている場合があります。

  • 照明をオンにして、タイマーの後にオフにする。
  • 一定の期間、照明を点滅させる。

これらの拡張機能の一部は、最小セットをサポートするデバイスでエミュレートできます。 これは、既定の実装を提供することを示します。 より多くの機能が組み込まれているデバイスの場合、デバイス ソフトウェアではネイティブ機能を使用します。 他の照明については、インターフェイスを実装し、既定の実装を使用することを選択できます。

このシナリオでは、拡張メソッドよりも既定のインターフェイス メンバーの方が適したソリューションを提供します。 クラス作成者は、実装するインターフェイスを制御できます。 選択したインターフェイスはメソッドとして利用できます。 また、既定のインターフェイス メソッドは既定で仮想であるため、メソッドのディスパッチでは常にクラス内の実装が選択されます。

これらの違いを示すコードを作成してみましょう。

インターフェイスを作成する

まず、すべての照明の動作を定義するインターフェイスを作成します。

public interface ILight
{
    void SwitchOn();
    void SwitchOff();
    bool IsOn();
}

基本的な天井の照明器具では、次のコードに示すようにこのインターフェイスを実装する場合があります。

public class OverheadLight : ILight
{
    private bool isOn;
    public bool IsOn() => isOn;
    public void SwitchOff() => isOn = false;
    public void SwitchOn() => isOn = true;

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

このチュートリアルでは、IoT デバイスを駆動していませんが、コンソールにメッセージを書き込んでそのアクティビティをエミュレートします。 家を自動化せずにコードを調べることができます。

次に、タイムアウト後に自動的にオフにできる照明のインターフェイスを定義します。

public interface ITimerLight : ILight
{
    Task TurnOnFor(int duration);
}

天井の照明に基本的な実装を追加することもできますが、おすすめのソリューションは、このインターフェイス定義を変更して virtual の既定の実装を提供することです。

public interface ITimerLight : ILight
{
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Using the default interface method for the ITimerLight.TurnOnFor.");
        SwitchOn();
        await Task.Delay(duration);
        SwitchOff();
        Console.WriteLine("Completed ITimerLight.TurnOnFor sequence.");
    }
}

OverheadLight クラスでは、インターフェイスのサポートを宣言することで、タイマー関数を実装できます。

public class OverheadLight : ITimerLight { }

照明の種類によっては、より高度なプロトコルがサポートされている場合があります。 次のコードに示すように、TurnOnFor の独自の実装を提供できます。

public class HalogenLight : ITimerLight
{
    private enum HalogenLightState
    {
        Off,
        On,
        TimerModeOn
    }

    private HalogenLightState state;
    public void SwitchOn() => state = HalogenLightState.On;
    public void SwitchOff() => state = HalogenLightState.Off;
    public bool IsOn() => state != HalogenLightState.Off;
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Halogen light starting timer function.");
        state = HalogenLightState.TimerModeOn;
        await Task.Delay(duration);
        state = HalogenLightState.Off;
        Console.WriteLine("Halogen light finished custom timer function");
    }

    public override string ToString() => $"The light is {state}";
}

仮想クラス メソッドのオーバーライドとは異なり、HalogenLight クラスの TurnOnFor の宣言では、override キーワードは使用されません。

機能の混在と対応付け

より高度な機能を導入すると、既定のインターフェイス メソッドの利点が明らかになります。 インターフェイスを使用すると、機能を混在させて対応付けることができます。 また、各クラスの作成者は、既定の実装とカスタム実装のいずれかを選択できるようになります。 点滅する照明の既定の実装を持つインターフェイスを追加してみましょう。

public interface IBlinkingLight : ILight
{
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("Using the default interface method for IBlinkingLight.Blink.");
        for (int count = 0; count < repeatCount; count++)
        {
            SwitchOn();
            await Task.Delay(duration);
            SwitchOff();
            await Task.Delay(duration);
        }
        Console.WriteLine("Done with the default interface method for IBlinkingLight.Blink.");
    }
}

既定の実装では、任意の照明を点滅させることができます。 天井の照明には、既定の実装を使用して、タイマーと点滅の両方の機能を追加できます。

public class OverheadLight : ILight, ITimerLight, IBlinkingLight
{
    private bool isOn;
    public bool IsOn() => isOn;
    public void SwitchOff() => isOn = false;
    public void SwitchOn() => isOn = true;

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

新しい照明の種類である LEDLight では、タイマー関数と点滅関数の両方を直接サポートします。 この照明のスタイルでは、ITimerLightIBlinkingLight の両方のインターフェイスを実装し、Blink メソッドをオーバーライドします。

public class LEDLight : IBlinkingLight, ITimerLight, ILight
{
    private bool isOn;
    public void SwitchOn() => isOn = true;
    public void SwitchOff() => isOn = false;
    public bool IsOn() => isOn;
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("LED Light starting the Blink function.");
        await Task.Delay(duration * repeatCount);
        Console.WriteLine("LED Light has finished the Blink function.");
    }

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

ExtraFancyLight は、点滅とタイマーの両方の関数を直接サポートしている可能性があります。

public class ExtraFancyLight : IBlinkingLight, ITimerLight, ILight
{
    private bool isOn;
    public void SwitchOn() => isOn = true;
    public void SwitchOff() => isOn = false;
    public bool IsOn() => isOn;
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("Extra Fancy Light starting the Blink function.");
        await Task.Delay(duration * repeatCount);
        Console.WriteLine("Extra Fancy Light has finished the Blink function.");
    }
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Extra Fancy light starting timer function.");
        await Task.Delay(duration);
        Console.WriteLine("Extra Fancy light finished custom timer function");
    }

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

以前に作成した HalogenLight は、点滅をサポートしていません。 そのため、サポートされているインターフェイスの一覧に IBlinkingLight を追加しないでください。

パターン マッチングを使用して照明の種類を検出する

次に、テスト コードを書いてみましょう。 C# のパターン マッチング機能を利用して、サポートされているインターフェイスを調べることで、照明の機能を決定することができます。 次のメソッドでは、各照明のサポートされている機能を実行します。

private static async Task TestLightCapabilities(ILight light)
{
    // Perform basic tests:
    light.SwitchOn();
    Console.WriteLine($"\tAfter switching on, the light is {(light.IsOn() ? "on" : "off")}");
    light.SwitchOff();
    Console.WriteLine($"\tAfter switching off, the light is {(light.IsOn() ? "on" : "off")}");

    if (light is ITimerLight timer)
    {
        Console.WriteLine("\tTesting timer function");
        await timer.TurnOnFor(1000);
        Console.WriteLine("\tTimer function completed");
    }
    else
    {
        Console.WriteLine("\tTimer function not supported.");
    }

    if (light is IBlinkingLight blinker)
    {
        Console.WriteLine("\tTesting blinking function");
        await blinker.Blink(500, 5);
        Console.WriteLine("\tBlink function completed");
    }
    else
    {
        Console.WriteLine("\tBlink function not supported.");
    }
}

Main メソッドの次のコードでは、各照明の種類を順番に作成し、その照明をテストします。

static async Task Main(string[] args)
{
    Console.WriteLine("Testing the overhead light");
    var overhead = new OverheadLight();
    await TestLightCapabilities(overhead);
    Console.WriteLine();

    Console.WriteLine("Testing the halogen light");
    var halogen = new HalogenLight();
    await TestLightCapabilities(halogen);
    Console.WriteLine();

    Console.WriteLine("Testing the LED light");
    var led = new LEDLight();
    await TestLightCapabilities(led);
    Console.WriteLine();

    Console.WriteLine("Testing the fancy light");
    var fancy = new ExtraFancyLight();
    await TestLightCapabilities(fancy);
    Console.WriteLine();
}

コンパイラで最適な実装を決定する方法

このシナリオは、実装のない基底インターフェイスを示しています。 ILight インターフェイスにメソッドを追加すると、新たな複雑さが生じます。 既定のインターフェイス メソッドを管理する言語規則により、複数の派生インターフェイスを実装する具象クラスへの影響が最小限に抑えられます。 元のインターフェイスを新しいメソッドで拡張して、その使用方法がどのように変わるかを説明します。 すべてのインジケーター照明では、その電源の状態を列挙値として報告できます。

public enum PowerStatus
{
    NoPower,
    ACPower,
    FullBattery,
    MidBattery,
    LowBattery
}

既定の実装では、電源は想定されていません。

public interface ILight
{
    void SwitchOn();
    void SwitchOff();
    bool IsOn();
    public PowerStatus Power() => PowerStatus.NoPower;
}

ExtraFancyLightILight インターフェイスと ITimerLight および IBlinkingLight 両方の派生インターフェイスのサポートを宣言していても、これらの変更は正常にコンパイルされます。 ILight インターフェイスで宣言される "最も近い" 実装は 1 つのみです。 オーバーライドを宣言した任意のクラスは、1 つの "最も近い" 実装になります。 上記のクラスの例では、他の派生インターフェイスのメンバーをオーバーライドしています。

複数の派生インターフェイスで同じメソッドをオーバーライドすることは避けてください。 このようにすると、クラスで両方の派生インターフェイスが実装されるたびに、あいまいなメソッド呼び出しが作成されます。 コンパイラでは、より適切な 1 つのメソッドを選択できないため、エラーが発行されます。 たとえば、IBlinkingLightITimerLight の両方で PowerStatus のオーバーライドが実装されている場合、OverheadLight にはより具体的なオーバーライドを用意する必要があります。 そうしないと、コンパイラでは 2 つの派生インターフェイスで実装を選択できません。 通常、インターフェイスの定義を小さく保ち、1 つの機能に集中することで、この状況を回避できます。 このシナリオでは、照明の各機能は独自のインターフェイスです。クラスからのみ複数のインターフェイスが継承されます。

このサンプルでは、クラスに混在させることができる個別の機能を定義できる 1 つのシナリオを示します。 サポートされる機能のセットを宣言するには、クラスがサポートするインターフェイスを宣言します。 仮想の既定のインターフェイス メソッドを使用すると、クラスでは、任意の、またはすべてのインターフェイス メソッドに対して異なる実装を使用または定義できます。 この言語機能によって、構築している実際のシステムをモデル化する新しい方法が提供されます。 既定のインターフェイス メソッドでは、これらの機能の仮想実装を使用して、さまざまな機能を混在させて対応付けることができる関連クラスをより明確に表現できるようになります。