チュートリアル: 既定のインターフェイス メソッドでインターフェイスを使用してクラスを作成するときの機能の混合Tutorial: Mix in functionality when creating classes using interfaces with default interface methods

.NET Core 3.0 上の C# 8.0 以降では、インターフェイスのメンバーを宣言するときに実装を定義できます。Beginning with C# 8.0 on .NET Core 3.0, you can define an implementation when you declare a member of an interface. この機能により、インターフェイスで宣言された機能の既定の実装を定義できるという新機能が提供されます。This feature provides new capabilities where you can define default implementations for features declared in interfaces. クラスでは、機能をオーバーライドする場合、既定の機能を使用する場合、および個別の機能のサポートを宣言しない場合を選択できます。Classes can pick when to override functionality, when to use the default functionality, and when not to declare support for discrete features.

このチュートリアルでは、次の作業を行う方法について説明します。In this tutorial, you'll learn how to:

  • 個別の機能を記述する実装を備えたインターフェイスを作成します。Create interfaces with implementations that describe discrete features.
  • 既定の実装を使用するクラスを作成します。Create classes that use the default implementations.
  • 既定の実装の一部またはすべてをオーバーライドするクラスを作成します。Create classes that override some or all of the default implementations.

必須コンポーネントPrerequisites

お使いのコンピューターを、.NET Core が実行されるように設定する必要があります。C# 8.0 コンパイラも実行されるようにします。You’ll need to set up your machine to run .NET Core, including the C# 8.0 compiler. C# 8.0 コンパイラは Visual Studio 2019 16.3 または .NET Core 3.0 SDK 以降で使用できます。The C# 8.0 compiler is available starting with Visual Studio 2019, 16.3, or the .NET Core 3.0 SDK or later.

拡張メソッドの制限事項Limitations of extension methods

インターフェイスの一部として表示される動作を実装する方法の 1 つとして、既定の動作を提供する拡張メソッドを定義することがあります。One way you can implement behavior that appears as part of an interface is to define extension methods that provide the default behavior. インターフェイスでは、そのインターフェイスを実装するクラスに対してより広い範囲の外部からのアクセスを提供すると同時に、最小セットのメンバーを宣言します。Interfaces declare a minimum set of members while providing a greater surface area for any class that implements that interface. たとえば、Enumerable の拡張メソッドは、任意のシーケンスの実装を LINQ クエリのソースとして提供します。For example, the extension methods in Enumerable provide the implementation for any sequence to be the source of a LINQ query.

拡張メソッドは、コンパイル時に変数の宣言型を使用して解決されます。Extension methods are resolved at compile time, using the declared type of the variable. インターフェイスを実装するクラスを使用すると、すべての拡張メソッドに対してより適切な実装を提供できます。Classes that implement the interface can provide a better implementation for any extension method. コンパイラで実装を選択できるようにするには、変数宣言が実装する型と一致している必要があります。Variable declarations must match the implementing type to enable the compiler to choose that implementation. コンパイル時の型がインターフェイスと一致する場合、メソッドの呼び出しは拡張メソッドに解決されます。When the compile-time type matches the interface, method calls resolve to the extension method. 拡張メソッドに関するもう 1 つの問題は、拡張メソッドを含むクラスにアクセスできる場所であれば、これらのメソッドにアクセスできることです。Another concern with extension methods is that those methods are accessible wherever the class containing the extension methods is accessible. クラスでは、拡張メソッドで宣言された機能を提供する必要があるかどうかを宣言できません。Classes cannot declare if they should or should not provide features declared in extension methods.

C# 8.0 以降では、既定の実装をインターフェイス メソッドとして宣言できます。Starting with C# 8.0, you can declare the default implementations as interface methods. その後は、すべてのクラスで自動的に既定の実装が使用されます。Then, every class automatically uses the default implementation. より適切な実装を提供できるクラスでは、インターフェイス メソッドの定義をより適切なアルゴリズムでオーバーライドできます。Any class that can provide a better implementation can override the interface method definition with a better algorithm. ある意味では、この手法は拡張メソッドを使用する方法と似ています。In one sense, this technique sounds similar to how you could use extension methods.

この記事では、既定のインターフェイスの実装で新しいシナリオを実現する方法について説明します。In this article, you'll learn how default interface implementations enable new scenarios.

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

ホーム オートメーション アプリケーションについて考えてみましょう。Consider a home automation application. 家庭内で使用される可能性がある照明とインジケーターには、おそらくさまざまな種類があります。You probably have many different types of lights and indicators that could be used throughout the house. すべての照明が、オン/オフを切り替え、現在の状態を報告する API をサポートする必要があります。Every light must support APIs to turn them on and off, and to report the current state. 一部の照明とインジケーターでは、次のような他の機能がサポートされている場合があります。Some lights and indicators may support other features, such as:

  • 照明をオンにして、タイマーの後にオフにする。Turn light on, then turn it off after a timer.
  • 一定の期間、照明を点滅させる。Blink the light for a period of time.

これらの拡張機能の一部は、最小セットをサポートするデバイスでエミュレートできます。Some of these extended capabilities could be emulated in devices that support the minimal set. これは、既定の実装を提供することを示します。That indicates providing a default implementation. より多くの機能が組み込まれているデバイスの場合、デバイス ソフトウェアではネイティブ機能を使用します。For those devices that have more capabilities built in, the device software would use the native capabilities. 他の照明については、インターフェイスを実装し、既定の実装を使用することを選択できます。For other lights, they could choose to implement the interface and use the default implementation.

このシナリオでは、拡張メソッドよりも既定のインターフェイス メンバーの方が適したソリューションです。Default interface members is a better solution for this scenario than extension methods. クラス作成者は、実装するインターフェイスを制御できます。Class authors can control which interfaces they choose to implement. 選択したインターフェイスはメソッドとして利用できます。Those interfaces they choose are available as methods. また、既定のインターフェイス メソッドは既定で仮想であるため、メソッドのディスパッチでは常にクラス内の実装が選択されます。In addition, because default interface methods are virtual by default, the method dispatch always chooses the implementation in the class.

これらの違いを示すコードを作成してみましょう。Let's create the code to demonstrate these differences.

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

まず、すべての照明の動作を定義するインターフェイスを作成します。Start by creating the interface that defines the behavior for all lights:

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

基本的な天井の照明器具では、次のコードに示すようにこのインターフェイスを実装する場合があります。A basic overhead light fixture might implement this interface as shown in the following code:

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 デバイスを駆動していませんが、コンソールにメッセージを書き込んでそのアクティビティをエミュレートします。In this tutorial, the code doesn't drive IoT devices, but emulates those activities by writing messages to the console. 家を自動化せずにコードを調べることができます。You can explore the code without automating your house.

次に、タイムアウト後に自動的にオフにできる照明のインターフェイスを定義します。Next, let's define the interface for a light that can automatically turn off after a timeout:

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

天井の照明に基本的な実装を追加することもできますが、おすすめのソリューションは、このインターフェイス定義を変更して virtual の既定の実装を提供することです。You could add a basic implementation to the overhead light, but a better solution is to modify this interface definition to provide a virtual default implementation:

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 クラスで、インターフェイスのサポートを宣言してタイマー関数を実装できます。By adding that change, the OverheadLight class can implement the timer function by declaring support for the interface:

public class OverheadLight : ITimerLight { }

照明の種類によっては、より高度なプロトコルがサポートされている場合があります。A different light type may support a more sophisticated protocol. 次のコードに示すように、TurnOnFor の独自の実装を提供できます。It can provide its own implementation for TurnOnFor, as shown in the following code:

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 キーワードは使用されません。Unlike overriding virtual class methods, the declaration of TurnOnFor in the HalogenLight class does not use the override keyword.

機能の混在と対応付けMix and match capabilities

より高度な機能を導入すると、既定のインターフェイス メソッドの利点が明らかになります。The advantages of default interface methods become clearer as you introduce more advanced capabilities. インターフェイスを使用すると、機能を混在させて対応付けることができます。Using interfaces enables you to mix and match capabilities. また、各クラスの作成者は、既定の実装とカスタム実装のいずれかを選択できるようになります。It also enables each class author to choose between the default implementation and a custom implementation. 点滅する照明の既定の実装を持つインターフェイスを追加してみましょう。Let's add an interface with a default implementation for a blinking light:

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

既定の実装では、任意の照明を点滅させることができます。The default implementation enables any light to blink. 天井の照明には、既定の実装を使用して、タイマーと点滅の両方の機能を追加できます。The overhead light can add both timer and blink capabilities using the default implementation:

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 では、タイマー関数と点滅関数の両方を直接サポートします。A new light type, the LEDLight supports both the timer function and the blink function directly. この照明のスタイルでは、ITimerLightIBlinkingLight の両方のインターフェイスを実装し、Blink メソッドをオーバーライドします。This light style implements both the ITimerLight and IBlinkingLight interfaces, and overrides the Blink method:

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 funtion.");
    }

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

ExtraFancyLight は、点滅とタイマーの両方の関数を直接サポートしている可能性があります。An ExtraFancyLight might support both blink and timer functions directly:

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 funtion.");
    }
    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 は、点滅をサポートしていません。The HalogenLight you created earlier doesn't support blinking. そのため、サポートされているインターフェイスの一覧に IBlinkingLight を追加しないでください。So, don't add the IBlinkingLight to the list of its supported interfaces.

パターン マッチングを使用して照明の種類を検出するDetect the light types using pattern matching

次に、テスト コードを書いてみましょう。Next, let's write some test code. C# のパターン マッチング機能を利用して、サポートされているインターフェイスを調べることで、照明の機能を決定することができます。You can make use of C#'s pattern matching feature to determine a light's capabilities by examining which interfaces it supports. 次のメソッドでは、各照明のサポートされている機能を実行します。The following method exercises the supported capabilities of each light:

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 メソッドの次のコードでは、各照明の種類を順番に作成し、その照明をテストします。The following code in your Main method creates each light type in sequence and tests that light:

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

コンパイラで最適な実装を決定する方法How the compiler determines best implementation

このシナリオは、実装のない基底インターフェイスを示しています。This scenario shows a base interface without any implementations. ILight インターフェイスにメソッドを追加すると、新たな複雑さが生じます。Adding a method into the ILight interface introduces new complexities. 既定のインターフェイス メソッドを管理する言語規則により、複数の派生インターフェイスを実装する具象クラスへの影響が最小限に抑えられます。The language rules governing default interface methods minimize the effect on the concrete classes that implement multiple derived interfaces. 元のインターフェイスを新しいメソッドで拡張して、その使用方法がどのように変わるかを説明します。Let's enhance the original interface with a new method to show how that changes its use. すべてのインジケーター照明では、その電源の状態を列挙値として報告できます。Every indicator light can report its power status as an enumerated value:

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

既定の実装では AC 電源が想定されています。The default implementation assumes AC power:

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

ExtraFancyLightILight インターフェイスと ITimerLight および IBlinkingLight 両方の派生インターフェイスのサポートを宣言していても、これらの変更は正常にコンパイルされます。These changes compile cleanly, even though the ExtraFancyLight declares support for the ILight interface and both derived interfaces, ITimerLight and IBlinkingLight. ILight インターフェイスで宣言される "最も近い" 実装は 1 つのみです。There's only one "closest" implementation declared in the ILight interface. オーバーライドを宣言した任意のクラスは、1 つの "最も近い" 実装になります。Any class that declared an override would become the one "closest" implementation. 上記のクラスの例では、他の派生インターフェイスのメンバーをオーバーライドしています。You saw examples in the preceding classes that overrode the members of other derived interfaces.

複数の派生インターフェイスで同じメソッドをオーバーライドすることは避けてください。Avoid overriding the same method in multiple derived interfaces. このようにすると、クラスで両方の派生インターフェイスが実装されるたびに、あいまいなメソッド呼び出しが作成されます。Doing so creates an ambiguous method call whenever a class implements both derived interfaces. コンパイラでは、より適切な 1 つのメソッドを選択できないため、エラーが発行されます。The compiler can't pick a single better method so it issues an error. たとえば、IBlinkingLightITimerLight の両方で PowerStatus のオーバーライドが実装されている場合、OverheadLight にはより具体的なオーバーライドを用意する必要があります。For example, if both the IBlinkingLight and ITimerLight implemented an override of PowerStatus, the OverheadLight would need to provide a more specific override. そうしないと、コンパイラでは 2 つの派生インターフェイスで実装を選択できません。Otherwise, the compiler can't pick between the implementations in the two derived interfaces. 通常、インターフェイスの定義を小さく保ち、1 つの機能に集中することで、この状況を回避できます。You can usually avoid this situation by keeping interface definitions small and focused on one feature. このシナリオでは、照明の各機能は独自のインターフェイスです。複数のインターフェイスはクラスからのみ継承されます。In this scenario, each capability of a light is its own interface; multiple interfaces are only inherited by classes.

このサンプルでは、クラスに混在させることができる個別の機能を定義できる 1 つのシナリオを示します。This sample shows one scenario where you can define discrete features that can be mixed into classes. サポートされる機能のセットを宣言するには、クラスがサポートするインターフェイスを宣言します。You declare any set of supported functionality by declaring which interfaces a class supports. 仮想の既定のインターフェイス メソッドを使用すると、クラスでは、任意の、またはすべてのインターフェイス メソッドに対して異なる実装を使用または定義できます。The use of virtual default interface methods enables classes to use or define a different implementation for any or all the interface methods. この言語機能によって、構築している実際のシステムをモデル化する新しい方法が提供されます。This language capability provides new ways to model the real-world systems you're building. 既定のインターフェイス メソッドでは、これらの機能の仮想実装を使用して、さまざまな機能を混在させて対応付けることができる関連クラスをより明確に表現できるようになります。Default interface methods provide a clearer way to express related classes that may mix and match different features using virtual implementations of those capabilities.