Självstudie: Blanda funktioner i när du skapar klasser med hjälp av gränssnitt med standardgränssnittsmetoder

Du kan definiera en implementering när du deklarerar en medlem i ett gränssnitt. Den här funktionen innehåller nya funktioner där du kan definiera standardimplementeringar för funktioner som deklareras i gränssnitt. Klasser kan välja när funktioner ska åsidosättas, när standardfunktionerna ska användas och när de inte ska deklarera stöd för diskreta funktioner.

I den här självstudien får du lära dig att:

  • Skapa gränssnitt med implementeringar som beskriver diskreta funktioner.
  • Skapa klasser som använder standardimplementeringarna.
  • Skapa klasser som åsidosätter vissa eller alla standardimplementeringar.

Förutsättningar

Du måste konfigurera datorn för att köra .NET, inklusive C#-kompilatorn. C#-kompilatorn är tillgänglig med Visual Studio 2022 eller .NET SDK.

Begränsningar för tilläggsmetoder

Ett sätt att implementera beteenden som visas som en del av ett gränssnitt är att definiera tilläggsmetoder som tillhandahåller standardbeteendet. Gränssnitt deklarerar en minsta uppsättning medlemmar samtidigt som en större yta för alla klasser som implementerar gränssnittet. Till exempel tillhandahåller tilläggsmetoderna i Enumerable implementeringen för alla sekvenser som ska vara källan till en LINQ-fråga.

Tilläggsmetoder löses vid kompileringstillfället med hjälp av variabelns deklarerade typ. Klasser som implementerar gränssnittet kan ge en bättre implementering för alla tilläggsmetoder. Variabeldeklarationer måste matcha implementeringstypen för att kompilatorn ska kunna välja den implementeringen. När kompileringstidstypen matchar gränssnittet matchas metodanrop till tilläggsmetoden. Ett annat problem med tilläggsmetoder är att dessa metoder är tillgängliga varhelst klassen som innehåller tilläggsmetoderna är tillgänglig. Klasser kan inte deklarera om de ska eller inte ska tillhandahålla funktioner som deklarerats i tilläggsmetoder.

Du kan deklarera standardimplementeringarna som gränssnittsmetoder. Sedan använder varje klass automatiskt standardimplementeringen. Alla klasser som kan tillhandahålla en bättre implementering kan åsidosätta gränssnittsmetoddefinitionen med en bättre algoritm. På ett sätt låter den här tekniken ungefär som hur du kan använda tilläggsmetoder.

I den här artikeln får du lära dig hur standardgränssnittsimplementeringar möjliggör nya scenarier.

Utforma programmet

Överväg ett hemautomatiseringsprogram. Du har förmodligen många olika typer av lampor och indikatorer som kan användas i hela huset. Varje ljus måste ha stöd för API:er för att aktivera och inaktivera dem och rapportera det aktuella tillståndet. Vissa lampor och indikatorer kan ha stöd för andra funktioner, till exempel:

  • Tänd lampan och stäng sedan av den efter en timer.
  • Blinka ljuset under en tidsperiod.

Vissa av dessa utökade funktioner kan emuleras på enheter som stöder den minimala uppsättningen. Det anger att du tillhandahåller en standardimplementering. För de enheter som har fler inbyggda funktioner använder enhetsprogramvaran de inbyggda funktionerna. För andra lampor kan de välja att implementera gränssnittet och använda standardimplementeringen.

Standardgränssnittsmedlemmar ger en bättre lösning för det här scenariot än tilläggsmetoder. Klassförfattare kan styra vilka gränssnitt de väljer att implementera. De gränssnitt som de väljer är tillgängliga som metoder. Eftersom standardgränssnittsmetoderna är virtuella som standard väljer metoden dispatch alltid implementeringen i klassen.

Nu ska vi skapa koden för att demonstrera dessa skillnader.

Skapa gränssnitt

Börja med att skapa gränssnittet som definierar beteendet för alla lampor:

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

En grundläggande overhead-armatur kan implementera det här gränssnittet enligt följande kod:

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

I den här självstudien kör koden inte IoT-enheter, utan emulerar dessa aktiviteter genom att skriva meddelanden till konsolen. Du kan utforska koden utan att automatisera ditt hus.

Nu ska vi definiera gränssnittet för en lampa som automatiskt kan stängas av efter en timeout:

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

Du kan lägga till en grundläggande implementering i overhead-ljuset, men en bättre lösning är att ändra den här gränssnittsdefinitionen för att tillhandahålla en virtual standardimplementering:

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

Klassen OverheadLight kan implementera timerfunktionen genom att deklarera stöd för gränssnittet:

public class OverheadLight : ITimerLight { }

En annan ljustyp kan ha stöd för ett mer avancerat protokoll. Den kan tillhandahålla en egen implementering för TurnOnFor, enligt följande kod:

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

Till skillnad från åsidosättande metoder för virtuell klass använder deklarationen HalogenLight av TurnOnFor i klassen inte nyckelordetoverride.

Mixa och matcha funktioner

Fördelarna med standardgränssnittsmetoder blir tydligare när du introducerar mer avancerade funktioner. Med hjälp av gränssnitt kan du blanda och matcha funktioner. Det gör också att varje klassförfattare kan välja mellan standardimplementeringen och en anpassad implementering. Nu ska vi lägga till ett gränssnitt med en standardimplementering för ett blinkande ljus:

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

Standardimplementeringen gör att alla ljus kan blinka. Overheadlampan kan lägga till både timer- och blinkfunktioner med hjälp av standardimplementeringen:

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

En ny ljustyp, LEDLight stöder både timerfunktionen och blinkfunktionen direkt. Den här ljusstilen implementerar både gränssnitten ITimerLight och IBlinkingLight och åsidosätter Blink metoden:

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

En ExtraFancyLight kan ha stöd för både blink- och timerfunktioner direkt:

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

Det HalogenLight du skapade tidigare stöder inte blinkande. Lägg därför inte till i IBlinkingLight listan över gränssnitt som stöds.

Identifiera ljustyperna med hjälp av mönstermatchning

Nu ska vi skriva lite testkod. Du kan använda C#:s mönstermatchningsfunktion för att fastställa ett ljuss funktioner genom att undersöka vilka gränssnitt som stöds. Följande metod använder de funktioner som stöds för varje ljus:

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

Följande kod i din Main metod skapar varje ljustyp i sekvens och testar det ljuset:

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

Så här avgör kompilatorn bästa implementering

Det här scenariot visar ett basgränssnitt utan implementeringar. Genom att lägga till en metod i ILight gränssnittet introduceras nya komplexiteter. Språkreglerna som styr standardgränssnittsmetoderna minimerar effekten på de konkreta klasser som implementerar flera härledda gränssnitt. Nu ska vi förbättra det ursprungliga gränssnittet med en ny metod för att visa hur det ändrar dess användning. Varje indikatorlampa kan rapportera dess energistatus som ett uppräknat värde:

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

Standardimplementeringen förutsätter ingen effekt:

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

Dessa ändringar kompileras korrekt, även om ExtraFancyLight deklarerar stöd för ILight gränssnittet och båda härledda gränssnitten och ITimerLightIBlinkingLight. Det finns bara en "närmaste" implementering som deklareras i ILight gränssnittet. Alla klasser som deklarerade en åsidosättning skulle bli den "närmaste" implementeringen. Du såg exempel i föregående klasser som överskrör medlemmarna i andra härledda gränssnitt.

Undvik att åsidosätta samma metod i flera härledda gränssnitt. Detta skapar ett tvetydigt metodanrop när en klass implementerar båda härledda gränssnitten. Kompilatorn kan inte välja en enda bättre metod så det uppstår ett fel. Om både IBlinkingLight och ITimerLight implementerade en åsidosättning av PowerStatusskulle OverheadLight du till exempel behöva ange en mer specifik åsidosättning. Annars kan kompilatorn inte välja mellan implementeringarna i de två härledda gränssnitten. Du kan vanligtvis undvika den här situationen genom att hålla gränssnittsdefinitionerna små och fokusera på en funktion. I det här scenariot är varje ljusfunktion ett eget gränssnitt. endast klasser ärver flera gränssnitt.

Det här exemplet visar ett scenario där du kan definiera diskreta funktioner som kan blandas i klasser. Du deklarerar alla funktioner som stöds genom att deklarera vilka gränssnitt som en klass stöder. Med hjälp av virtuella standardgränssnittsmetoder kan klasser använda eller definiera en annan implementering för någon eller alla gränssnittsmetoder. Den här språkfunktionen ger nya sätt att modellera de verkliga system som du skapar. Standardgränssnittsmetoder ger ett tydligare sätt att uttrycka relaterade klasser som kan blanda och matcha olika funktioner med hjälp av virtuella implementeringar av dessa funktioner.