Zelfstudie: Functionaliteit combineren in 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.
  • Klassen maken die gebruikmaken van de standaard implementaties.
  • Klassen maken die sommige of alle standaard implementaties overschrijven.

Vereisten

U moet uw computer instellen voor het uitvoeren van .NET, inclusief de C#-compiler. De C#-compiler is beschikbaar met Visual Studio 2022 of de .NET SDK of hoger.

Beperkingen van extensiemethoden

Een manier waarop u gedrag kunt implementeren dat wordt weergegeven als onderdeel van een interface, is het definiëren van extensiemethoden 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 Enumerable bieden bijvoorbeeld de implementatie voor elke reeks als bron van een LINQ-query.

Extensiemethoden worden opgelost tijdens het compileren, met behulp van het opgegeven 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, wordt de methode 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 of ze wel of geen functies moeten bieden die zijn gedeclareerd in extensiemethoden.

U kunt de standaard implementaties declareren als interfacemethoden. Vervolgens maakt elke klasse automatisch gebruik van de standaard implementatie. Elke klasse die een betere implementatie kan bieden, kan de definitie van de interfacemethode overschrijven met een beter algoritme. In zekere zin lijkt deze techniek op de manier waarop u extensiemethoden kunt gebruiken.

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

De toepassing ontwerpen

Overweeg een domoticatoepassing. U hebt waarschijnlijk veel verschillende soorten lichten en indicatoren die door het hele huis kunnen worden gebruikt. Elk lampje moet API's ondersteunen om deze in en uit te schakelen en om de huidige status te rapporteren. Sommige lichten en indicatoren ondersteunen mogelijk andere functies, zoals:

  • Schakel het lampje in en schakel het uit na een timer.
  • Knipper het lampje gedurende een bepaalde tijd.

Sommige van deze uitgebreide mogelijkheden kunnen worden geëmuleerd in apparaten die ondersteuning bieden voor de minimale set. Dat geeft aan dat u een standaard implementatie moet opgeven. Voor apparaten die meer ingebouwde mogelijkheden hebben, gebruikt de apparaatsoftware de systeemeigen mogelijkheden. Voor andere lichten konden ze ervoor kiezen om de interface te implementeren en de standaard implementatie te gebruiken.

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

We gaan de code maken om deze verschillen te demonstreren.

Interfaces maken

Begin met het maken van de interface die het gedrag voor alle lichten definieert:

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

Een eenvoudige overheadlamp 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ëmuleerd door berichten naar de console te schrijven. U kunt de code verkennen zonder uw huis te automatiseren.

Laten we nu de interface definiëren voor een lampje 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.");
    }
}

Door deze wijziging toe te voegen, kan de OverheadLight klasse 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 virtuele-klassemethoden, gebruikt de declaratie van TurnOnFor in de HalogenLight klasse niet het override trefwoord.

Mogelijkheden voor mix en match

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

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 lampje knipperen. Het overheadlampje kan zowel timer- als knipperfuncties 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. Met deze lichte stijl worden zowel de ITimerLight interfaces en IBlinkingLight geïmplementeerd en wordt de Blink methode overschreven:

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 kan zowel knipper- 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 die u eerder hebt gemaakt, biedt geen ondersteuning voor knipperen. Voeg de IBlinkingLight dus niet toe aan de lijst met ondersteunde interfaces.

De lichttypen detecteren met behulp van patroonkoppeling

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

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 opeenvolgend 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

Dit scenario toont een basisinterface zonder implementaties. Als u een methode toevoegt aan de ILight interface, worden er 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 verandert. Elk indicatorlampje kan de energiestatus rapporteren als een geïnventareerde waarde:

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

Bij de standaard implementatie wordt ervan uitgegaan dat er geen stroom wordt gebruikt:

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

Deze wijzigingen worden netjes gecompileerd, ook al declareert de ExtraFancyLight ondersteuning voor de ILight interface en beide afgeleide interfaces, ITimerLight en IBlinkingLight. Er is slechts één 'dichtstbijzijnde' implementatie gedeclareerd in de ILight interface. Elke klasse die een onderdrukking heeft gedeclareerd, wordt de 'dichtstbijzijnde' implementatie. U hebt in de voorgaande klassen voorbeelden gezien die de leden van andere afgeleide interfaces overschreven.

Vermijd het overschrijven van dezelfde methode in meerdere afgeleide interfaces. Als u dit doet, 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 een onderdrukking van PowerStatushebben geïmplementeerd, moet de OverheadLight een specifiekere overschrijving opgeven. Anders kan de compiler niet kiezen tussen de implementaties in de twee afgeleide interfaces. U kunt deze situatie meestal voorkomen door interfacedefinities klein te houden en gericht te zijn op één functie. In dit scenario is elke mogelijkheid van een licht zijn eigen interface; meerdere interfaces worden alleen overgenomen door klassen.

In dit voorbeeld ziet u één scenario waarin u discrete functies kunt definiëren die in klassen kunnen worden gemengd. U declareert een set ondersteunde functionaliteit door op te geven 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 die u bouwt te modelleren. 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.