[C#翻译]教程。在使用带有默认接口方法的接口创建类时混合功能

228 阅读7分钟

本文由 简悦SimpRead 转码,原文地址 docs.microsoft.com

使用默认的接口成员,你可以用可选的默认实现来扩展接口i......。

从.NET Core 3.0上的C# 8.0开始,当你声明一个接口的成员时,你可以定义一个实现。这项功能提供了新的功能,你可以为接口中声明的功能定义默认的实现。类可以选择何时覆盖功能,何时使用默认功能,以及何时不声明对离散功能的支持。

在本教程中,你将学习如何。

  • 创建具有描述离散特性的实现的接口。
  • 创建使用默认实现的类。
  • 创建覆盖部分或全部默认实现的类。

先决条件

你需要设置你的机器来运行.NET核心,包括C# 8.0编译器。C# 8.0编译器从Visual Studio 2019 version 16.3,或.NET Core 3.0 SDK或更高版本开始可用。

扩展方法的限制

你可以实现作为接口的一部分出现的行为的一种方法是定义扩展方法,提供默认行为。接口声明了一个最小的成员集,同时为任何实现该接口的类提供了更大的表面积。例如,Enumerable的扩展方法为任何序列提供了实现,成为LINQ查询的来源。

扩展方法在编译时被解析,使用变量的声明类型。实现该接口的类可以为任何扩展方法提供更好的实现。变量的声明必须与实现类型相匹配,以使编译器能够选择该实现。当编译时的类型与接口匹配时,方法调用就会解析到扩展方法。扩展方法的另一个问题是,只要包含扩展方法的类可以访问,这些方法就可以访问。类不能声明它们是否应该或不应该提供扩展方法中声明的功能。

从C# 8.0开始,你可以将默认的实现声明为接口方法。然后,每个类都会自动使用默认的实现。任何能够提供更好实现的类都可以用更好的算法覆盖接口方法的定义。在某种意义上,这种技术听起来类似于你可以使用扩展方法

在这篇文章中,你将学习默认的接口实现如何实现新的场景。

设计应用程序

考虑一个家庭自动化应用。你可能有许多不同类型的灯和指示灯,可以在整个房子里使用。每个灯都必须支持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")}";
}

在本教程中,代码并不驱动物联网设备,而是通过向控制台写消息来模拟这些活动。你可以探索代码,而不需要将你的房子自动化。

接下来,让我们为一盏可以在超时后自动关闭的灯定义接口。

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

你可以给顶灯添加一个基本的实现,但更好的办法是修改这个接口定义,提供一个虚拟的默认实现。

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

    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添加到其支持的接口列表中。

[](#detect-the light-types-using-pattern-matching)使用模式匹配检测灯光类型

接下来,让我们写一些测试代码。你可以利用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;
}

这些变化编译得很干净,尽管ExtraFancyLight声明了对ILight接口和两个派生接口ITimerLightIBlinkingLight的支持。在 "ILight "接口中只声明了一个 "最接近 "的实现。任何声明覆盖的类都将成为一个 "最接近 "的实现。你在前面的类中看到了覆盖其他派生接口的成员的例子。

避免在多个派生接口中重写同一个方法。只要一个类同时实现了两个派生接口,这样做就会产生一个模糊的方法调用。编译器无法选择一个更好的方法,所以会发出一个错误。例如,如果IBlinkingLightITimerLight都实现了PowerStatus的覆盖,OverheadLight就需要提供一个更具体的覆盖。否则,编译器无法在这两个派生接口的实现之间进行选择。你通常可以通过保持接口定义的小规模和专注于一个功能来避免这种情况。在这种情况下,灯的每项功能都是它自己的接口;多个接口只能由类来继承。

这个例子展示了一种情况,你可以定义离散的功能,这些功能可以混合到类中。你通过声明一个类支持哪些接口来声明任何一组支持的功能。虚拟默认接口方法的使用使得类可以为任何或所有的接口方法使用或定义不同的实现。这种语言能力为你所构建的现实世界的系统提供了新的建模方式。默认接口方法提供了一种更清晰的方式来表达相关的类,这些类可能使用这些能力的虚拟实现来混合和匹配不同的功能。


www.deepl.com 翻译