Zelfstudie: Functionaliteit combineren bij het maken van klassen met behulp van interfaces met standaardinterfacemethoden

U kunt een implementatie definiëren wanneer u een lid van een interface declareert. Deze functie biedt nieuwe mogelijkheden waarmee u standaard implementaties kunt definiëren voor functies die zijn gedeclareerd in interfaces. Klassen kunnen kiezen wanneer functionaliteit moet worden overschreven, wanneer de standaardfunctionaliteit moet worden gebruikt en wanneer er geen ondersteuning voor discrete functies moet worden opgegeven.

In deze zelfstudie leert u het volgende:

  • Maak interfaces met implementaties die discrete functies beschrijven.
  • Maak klassen die gebruikmaken van de standaard implementaties.
  • Maak klassen die sommige of alle standaard implementaties overschrijven.

Vereisten

U moet uw computer instellen om .NET uit te voeren, inclusief de C#-compiler. De C#-compiler is beschikbaar met Visual Studio 2022 of de .NET SDK.

Beperkingen van extensiemethoden

Een manier waarop u gedrag kunt implementeren dat als onderdeel van een interface wordt weergegeven, is door extensiemethoden te definiëren die het standaardgedrag bieden. Interfaces declareren een minimale set leden en bieden een groter oppervlak voor elke klasse die die interface implementeert. De extensiemethoden in deze Enumerable methode bieden bijvoorbeeld de implementatie voor elke reeks die de bron van een LINQ-query moet zijn.

Extensiemethoden worden tijdens het compileren opgelost met behulp van het gedeclareerde type van de variabele. Klassen die de interface implementeren, kunnen een betere implementatie bieden voor elke extensiemethode. Variabeledeclaraties moeten overeenkomen met het implementatietype om de compiler in staat te stellen die implementatie te kiezen. Wanneer het type compileertijd overeenkomt met de interface, worden methode-aanroepen omgezet in de extensiemethode. Een ander probleem met extensiemethoden is dat deze methoden toegankelijk zijn waar de klasse met de extensiemethoden toegankelijk is. Klassen kunnen niet declareren als ze functies moeten bieden die zijn gedeclareerd in extensiemethoden.

U kunt de standaard implementaties declareren als interfacemethoden. Vervolgens gebruikt elke klasse automatisch de standaard implementatie. Elke klasse die een betere implementatie kan bieden, kan de definitie van de interfacemethode overschrijven met een beter algoritme. In één zin klinkt deze techniek vergelijkbaar met hoe u extensiemethoden kunt gebruiken.

In dit artikel leert u hoe standaardinterface-implementaties nieuwe scenario's mogelijk maken.

De toepassing ontwerpen

Overweeg een toepassing voor huisautomatisering. U hebt waarschijnlijk veel verschillende soorten lichten en indicatoren die overal in het huis kunnen worden gebruikt. Elk licht moet API's ondersteunen om ze in en uit te schakelen en om de huidige status te rapporteren. Sommige lichten en indicatoren ondersteunen mogelijk andere functies, zoals:

  • Schakel het licht in en schakel het uit na een timer.
  • Knipper het licht gedurende een bepaalde periode.

Sommige van deze uitgebreide mogelijkheden kunnen worden geëmuleerd op apparaten die ondersteuning bieden voor de minimale set. Dit geeft aan dat er een standaard implementatie wordt geboden. Voor apparaten die meer mogelijkheden hebben ingebouwd, zou de apparaatsoftware gebruikmaken van de systeemeigen mogelijkheden. Voor andere lichten kunnen ze ervoor kiezen om de interface te implementeren en de standaard implementatie te gebruiken.

Standaardinterfaceleden bieden een betere oplossing voor dit scenario dan extensiemethoden. Auteurs van klassen kunnen bepalen welke interfaces ze willen implementeren. Deze interfaces die ze kiezen, zijn beschikbaar als methoden. Omdat standaardinterfacemethoden standaard virtueel zijn, kiest de methode dispatch altijd de implementatie in de klasse.

Laten we de code maken om deze verschillen te demonstreren.

Interfaces maken

Begin met het maken van de interface waarmee het gedrag voor alle lichten wordt gedefinieerd:

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

Een eenvoudige overheadlichtarmatur kan deze interface implementeren, zoals wordt weergegeven in de volgende code:

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

In deze zelfstudie wordt met de code geen IoT-apparaten aangedreven, maar worden deze activiteiten geëmuleren door berichten naar de console te schrijven. U kunt de code verkennen zonder uw huis te automatiseren.

Laten we vervolgens de interface definiëren voor een licht dat automatisch kan worden uitgeschakeld na een time-out:

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

U kunt een eenvoudige implementatie toevoegen aan het overheadlampje, maar een betere oplossing is om deze interfacedefinitie te wijzigen om een virtual standaard implementatie te bieden:

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

De OverheadLight klasse kan de timerfunctie implementeren door ondersteuning voor de interface te declareren:

public class OverheadLight : ITimerLight { }

Een ander lichttype ondersteunt mogelijk een geavanceerder protocol. Het kan een eigen implementatie bieden voor TurnOnFor, zoals wordt weergegeven in de volgende code:

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

In tegenstelling tot het overschrijven van methoden voor virtuele klassen, wordt het trefwoord niet gebruikt override door de declaratie van TurnOnFor de HalogenLight klasse.

Mogelijkheden combineren en vergelijken

De voordelen van standaardinterfacemethoden worden duidelijker wanneer u geavanceerdere mogelijkheden introduceert. Met behulp van interfaces kunt u mogelijkheden combineren en vergelijken. Daarnaast kan elke auteur van de klasse kiezen tussen de standaard implementatie en een aangepaste implementatie. Laten we een interface met een standaard implementatie toevoegen voor een knipperend licht:

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

Met de standaard implementatie kan elk licht knipperen. Het overheadlampje kan zowel timer- als knippermogelijkheden toevoegen met behulp van de standaard implementatie:

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

Een nieuw lichttype, de LEDLight ondersteunt zowel de timerfunctie als de knipperfunctie rechtstreeks. Deze lichte stijl implementeert zowel de ITimerLight als IBlinkingLight de interfaces en overschrijft de Blink methode:

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

Een ExtraFancyLight functie kan zowel knipperen als timerfuncties rechtstreeks ondersteunen:

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

De HalogenLight eerder gemaakte versie biedt geen ondersteuning voor knipperen. Voeg de lijst met ondersteunde interfaces dus niet toe IBlinkingLight aan de lijst met ondersteunde interfaces.

De lichttypen detecteren met patroonkoppeling

Nu gaan we wat testcode schrijven. U kunt gebruikmaken van de patroonkoppelingsfunctie van C# om de mogelijkheden van een licht te bepalen door te onderzoeken welke interfaces het ondersteunt. Met de volgende methode worden de ondersteunde mogelijkheden van elk licht gebruikt:

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

Met de volgende code in uw Main methode wordt elk lichttype op volgorde gemaakt en wordt dat licht getest:

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

Hoe de compiler de beste implementatie bepaalt

In dit scenario ziet u een basisinterface zonder implementaties. Door een methode toe te voegen aan de ILight interface worden nieuwe complexiteiten geïntroduceerd. De taalregels voor standaardinterfacemethoden minimaliseren het effect op de concrete klassen die meerdere afgeleide interfaces implementeren. Laten we de oorspronkelijke interface verbeteren met een nieuwe methode om te laten zien hoe dit het gebruik ervan wijzigt. Elk indicatorlampje kan de energiestatus rapporteren als een opgesomde waarde:

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

Bij de standaard implementatie wordt ervan uitgegaan dat er geen vermogen is:

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

Deze wijzigingen worden op schone wijze gecompileerd, ook al declareert de ExtraFancyLight ondersteuning voor de ILight interface en zowel afgeleide interfaces als ITimerLightIBlinkingLight. Er is slechts één 'dichtstbijzijnde' implementatie die in de ILight interface is gedeclareerd. Elke klasse die een onderdrukking heeft gedeclareerd, wordt de dichtstbijzijnde implementatie. U hebt voorbeelden gezien in de voorgaande klassen die de leden van andere afgeleide interfaces overbelasten.

Vermijd het overschrijven van dezelfde methode in meerdere afgeleide interfaces. Hierdoor wordt een dubbelzinnige methodeaanroep gemaakt wanneer een klasse beide afgeleide interfaces implementeert. De compiler kan geen enkele betere methode kiezen, zodat er een fout optreedt. Als bijvoorbeeld zowel de IBlinkingLight als ITimerLight de implementatie van PowerStatuseen onderdrukking is, moet u OverheadLight een specifiekere overschrijving opgeven. Anders kan de compiler niet kiezen tussen de implementaties in de twee afgeleide interfaces. U kunt deze situatie meestal vermijden door interfacedefinities klein en gericht te houden op één functie. In dit scenario is elke mogelijkheid van een licht een eigen interface; alleen klassen nemen meerdere interfaces over.

In dit voorbeeld ziet u één scenario waarin u discrete functies kunt definiëren die kunnen worden gemengd in klassen. U declareert een set ondersteunde functionaliteit door te declareren welke interfaces een klasse ondersteunt. Met het gebruik van virtuele standaardinterfacemethoden kunnen klassen een andere implementatie gebruiken of definiëren voor een of alle interfacemethoden. Deze taalmogelijkheid biedt nieuwe manieren om de echte systemen te modelleren die u bouwt. Standaardinterfacemethoden bieden een duidelijkere manier om gerelateerde klassen uit te drukken die verschillende functies kunnen combineren en matchen met behulp van virtuele implementaties van deze mogelijkheden.