教程:当通过默认接口方法创建使用接口的类时实现的混入功能

可以在声明接口成员时定义实现。 此功能提供了一些新功能,可以在其中为接口中声明的功能定义默认实现。 类可以选择何时替代功能、何时使用默认功能以及何时不声明对离散功能的支持。

在本教程中,你将了解:

  • 使用描述离散功能的实现创建接口。
  • 创建使用默认实现的类。
  • 创建用于替代部分或全部默认实现的类。

先决条件

需要将计算机设置为运行 .NET,包括 C# 编译器。 Visual Studio 2022.NET SDK 随附 C# 编译器。

扩展方法的限制

实现作为接口一部分出现的行为的一种方法是:定义可提供默认行为的扩展方法。 接口声明最少的一组成员,同时为实现该接口的任何类提供更大的外围应用。 例如,Enumerable 中的扩展方法提供作为 LINQ 查询源的任何序列的实现。

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

可以将默认实现声明为接口方法。 然后,每个类将自动使用默认实现。 可提供更好实现的任何类都可以使用更好的算法来替代接口方法定义。 从某种意义上讲,这种技术听起来与你使用扩展方法的方式类似。

在本文中,你将了解默认接口实现如何启用新方案。

设计应用程序

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

即使 ExtraFancyLight 声明支持 ILight 接口以及派生接口 ITimerLightIBlinkingLight,这些更改仍会进行清晰的编译。 在 ILight 接口中只有一个声明为“最接近”的实现。 任何声明了替代的类都将成为一个“最接近”的实现。 你在前面的类中看到了替代其他派生接口成员的示例。

避免在多个派生接口中替代相同的方法。 这样做会在类实现两个派生接口时创建不明确的方法调用。 编译器不能选取一种更好的方法,因此会发出错误。 例如,如果 IBlinkingLightITimerLight 实现了 PowerStatus 的替代,则 OverheadLight 需要提供更具体的替代。 否则,编译器无法在两个派生接口的实现之间进行选择。 通常,为避免这种情况,可以使接口定义保持较小并专注于一个功能。 在此方案中,灯的每个功能都是其自己的接口;只有类可以继承多个接口。

此示例演示了一种方案,在该方案中可以定义可混合到类中的离散功能。 通过声明类支持的接口,可以声明任意一组受支持的功能。 使用虚拟默认接口方法使类可以使用或定义任何或所有接口方法的不同实现。 这种语言功能提供了对正在构建的真实系统进行建模的新方法。 默认接口方法提供了一种更清晰的方式来表达相关的类,这些类可能使用这些功能的虚拟实现来混合和匹配不同的功能。