教學課程:在使用具有預設介面方法的介面建立類別時,混合加入功能
您可以在宣告介面成員時定義實作。 這項新功能可讓您定義介面中所宣告功能的預設實作。 類別可以選擇何時覆寫功能、何時使用預設功能,以及何時不宣告支援離散功能。
在本教學課程中,您將了解如何:
- 使用描述離散功能的實作建立介面。
- 建立使用預設實作的類別。
- 建立會覆寫部分或所有預設實作的類別。
必要條件
您需要設定電腦,以執行 .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
可直接支援計時器函式和閃爍函式。 這個燈具樣式會同時實作 ITimerLight
和 IBlinkingLight
介面,並覆寫 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
介面以及兩個衍生介面 (ITimerLight
和 IBlinkingLight
),這些變更還是會完全編譯。 ILight
介面中只會宣告一個「最接近」的實作。 宣告覆寫的任何類別都會成為一個「最接近」的實作。 如同上述類別中的範例,這些範例會覆寫其他衍生介面的成員。
請避免在多個衍生介面中覆寫相同的方法。 每當有類別實作這兩個衍生介面時,就會建立模棱兩可的方法呼叫。 編譯器無法挑選出一個較好的方法,因此會發出錯誤。 例如,如果 IBlinkingLight
和 ITimerLight
都實作 PowerStatus
的覆寫,則 OverheadLight
需要提供更明確的覆寫。 否則,編譯器無法在兩個衍生介面中的實作之間進行選擇。 通常您可以透過限制介面定義並專注於一項功能,以避免這種情況。 在此案例中,燈具的每個功能都是自己的介面;只有類別會繼承多個介面。
此範例示範了一個案例,在其中定義可混合為類別的離散功能。 您可以宣告類別支援的介面,藉此宣告任何一組支援的功能。 透過虛擬預設介面方法,可讓類別針對任意或所有介面方法使用或定義不同的實作。 此語言功能可提供新的方式,協助您建立真實世界系統的模型。 預設介面方法會以更清楚的方式表達相關類別,而這些類別可使用這些功能的虛擬實作來混合和比對不同的功能。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應