教學課程:在使用具有預設介面方法的介面建立類別時,混合加入功能

您可以在宣告介面成員時定義實作。 這項新功能可讓您定義介面中所宣告功能的預設實作。 類別可以選擇何時覆寫功能、何時使用預設功能,以及何時不宣告支援離散功能。

在本教學課程中,您將了解如何:

  • 使用描述離散功能的實作建立介面。
  • 建立使用預設實作的類別。
  • 建立會覆寫部分或所有預設實作的類別。

必要條件

您需要設定電腦,以執行 .NET (包括 C# 編譯器)。 C# 編譯器可與 Visual Studio 2022.NET SDK 搭配使用。

擴充方法的限制

將行為實作為介面一部分的方式之一,就是定義提供預設行為的擴充方法。 介面會宣告最低限度的成員集,同時為實作該介面的任何類別提供更大的介面區。 例如,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 需要提供更明確的覆寫。 否則,編譯器無法在兩個衍生介面中的實作之間進行選擇。 通常您可以透過限制介面定義並專注於一項功能,以避免這種情況。 在此案例中,燈具的每個功能都是自己的介面;只有類別會繼承多個介面。

此範例示範了一個案例,在其中定義可混合為類別的離散功能。 您可以宣告類別支援的介面,藉此宣告任何一組支援的功能。 透過虛擬預設介面方法,可讓類別針對任意或所有介面方法使用或定義不同的實作。 此語言功能可提供新的方式,協助您建立真實世界系統的模型。 預設介面方法會以更清楚的方式表達相關類別,而這些類別可使用這些功能的虛擬實作來混合和比對不同的功能。