Kurz: Kombinování funkcí při vytváření tříd pomocí rozhraní s výchozími metodami rozhraní

Implementaci můžete definovat, když deklarujete člena rozhraní. Tato funkce poskytuje nové funkce, ve kterých můžete definovat výchozí implementace pro funkce deklarované v rozhraních. Třídy si můžou vybrat, kdy přepsat funkce, kdy použít výchozí funkce a kdy ne deklarovat podporu diskrétních funkcí.

V tomto kurzu se naučíte:

  • Vytvářejte rozhraní s implementacemi, které popisují samostatné funkce.
  • Vytvořte třídy, které používají výchozí implementace.
  • Vytvořte třídy, které přepíší některé nebo všechny výchozí implementace.

Požadavky

Musíte nastavit počítač tak, aby běžel na platformě .NET, včetně kompilátoru jazyka C#. Kompilátor jazyka C# je k dispozici se sadou Visual Studio 2022 nebo sadou .NET SDK.

Omezení rozšiřujících metod

Jedním ze způsobů, jak implementovat chování, které se zobrazí jako součást rozhraní, je definovat rozšiřující metody , které poskytují výchozí chování. Rozhraní deklarují minimální sadu členů a současně poskytují větší povrchovou plochu pro libovolnou třídu, která implementuje toto rozhraní. Například rozšiřující metody poskytují Enumerable implementaci pro libovolnou posloupnost, která bude zdrojem dotazu LINQ.

Metody rozšíření jsou vyřešeny v době kompilace pomocí deklarovaného typu proměnné. Třídy, které implementují rozhraní, mohou poskytovat lepší implementaci pro jakoukoli metodu rozšíření. Deklarace proměnných se musí shodovat s implementačním typem, aby kompilátor mohl tuto implementaci zvolit. Pokud typ kompilace odpovídá rozhraní, volání metody přeloží na metodu rozšíření. Dalším problémem s rozšiřujícími metodami je, že tyto metody jsou přístupné všude, kde je třída obsahující rozšiřující metody přístupná. Třídy nemůžou deklarovat, jestli by měly nebo neměly poskytovat funkce deklarované v rozšiřujících metodách.

Jako metody rozhraní můžete deklarovat výchozí implementace. Každá třída pak automaticky používá výchozí implementaci. Každá třída, která může poskytovat lepší implementaci, může přepsat definici metody rozhraní lepším algoritmem. V jednom smyslu tato technika zní podobně jako způsob použití rozšiřujících metod.

V tomto článku se dozvíte, jak výchozí implementace rozhraní umožňují nové scénáře.

Návrh aplikace

Zvažte aplikaci domácí automatizace. Pravděpodobně máte mnoho různých typů světel a indikátorů, které by mohly být použity v celém domě. Každé světlo musí podporovat rozhraní API, aby je zapnula a vypnula, a aby se nahlásil aktuální stav. Některá světla a indikátory můžou podporovat další funkce, například:

  • Zapněte světlo a vypněte ho po časovači.
  • Bliká světlo po určitou dobu.

Některé z těchto rozšířených funkcí je možné emulovat v zařízeních, která podporují minimální sadu. To znamená, že poskytuje výchozí implementaci. Pro zařízení s více integrovanými funkcemi by software zařízení používal nativní funkce. V případě jiných světel by se mohli rozhodnout implementovat rozhraní a použít výchozí implementaci.

Výchozí členy rozhraní poskytují lepší řešení pro tento scénář než metody rozšíření. Autoři tříd můžou určit, která rozhraní se rozhodnou implementovat. Tato rozhraní, která zvolí, jsou k dispozici jako metody. Vzhledem k tomu, že výchozí metody rozhraní jsou ve výchozím nastavení virtuální, volání metody vždy zvolí implementaci ve třídě.

Pojďme vytvořit kód, který tyto rozdíly předvede.

Vytváření rozhraní

Začněte vytvořením rozhraní, které definuje chování všech světel:

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

Základní režijní žárovka může implementovat toto rozhraní, jak je znázorněno v následujícím kódu:

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

V tomto kurzu kód nepodporuje zařízení IoT, ale emuluje tyto aktivity zápisem zpráv do konzoly. Kód můžete prozkoumat bez automatizace vašeho domu.

Teď nadefinujme rozhraní pro světlo, které se může po vypršení časového limitu automaticky vypnout:

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

Můžete přidat základní implementaci do režijního světla, ale lepším řešením je upravit tuto definici rozhraní tak, aby poskytovala virtual výchozí implementaci:

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

Třída OverheadLight může implementovat funkci časovače deklarováním podpory rozhraní:

public class OverheadLight : ITimerLight { }

Jiný světlý typ může podporovat sofistikovanější protokol. Může poskytnout vlastní implementaci pro TurnOnFor, jak je znázorněno v následujícím kódu:

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

Na rozdíl od přepsání metod TurnOnFor virtuální třídy deklarace třídy HalogenLight nepoužívá override klíčové slovo.

Možnosti kombinace a shody

Výhody výchozích metod rozhraní jsou jasnější při zavádění pokročilejších funkcí. Použití rozhraní umožňuje kombinovat a shodovat možnosti. Umožňuje také každému autorovi třídy zvolit mezi výchozí implementací a vlastní implementací. Pojďme přidat rozhraní s výchozí implementací pro blikající světlo:

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

Výchozí implementace umožňuje blikat jakékoli světlo. Režijní světlo může přidat funkce časovače i blikáku pomocí výchozí implementace:

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

Nový typ světla, LEDLight podporuje funkci časovače i bliká funkce přímo. Tento světlý styl implementuje obě ITimerLight rozhraní a IBlinkingLight přepíše metodu 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")}";
}

Funkce ExtraFancyLight blikáku i časovače můžou podporovat přímo:

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

Dříve HalogenLight vytvořený soubor nepodporuje blikající. Proto nepřidávejte IBlinkingLight seznam podporovaných rozhraní.

Detekce typů světla pomocí porovnávání vzorů

V dalším kroku napíšeme nějaký testovací kód. Pomocí funkce porovnávání vzorů jazyka C# můžete určit možnosti světla prozkoumáním rozhraní, která podporuje. Následující metoda provádí podporovaná nastavení jednotlivých světlů:

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

Následující kód ve vaší Main metodě vytvoří každý typ světla v sekvenci a testuje, že světlo:

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

Jak kompilátor určuje nejlepší implementaci

Tento scénář ukazuje základní rozhraní bez implementace. Přidání metody do ILight rozhraní zavádí nové složitosti. Pravidla jazyka, která řídí výchozí metody rozhraní, minimalizují vliv na konkrétní třídy, které implementují více odvozených rozhraní. Pojďme vylepšit původní rozhraní novou metodou, abychom ukázali, jak se změní jeho použití. Každé světlo indikátoru může hlásit svůj stav napájení jako výčtovou hodnotu:

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

Výchozí implementace předpokládá žádné napájení:

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

Tyto změny se kompilují čistě, i když ExtraFancyLight deklaruje podporu pro ILight rozhraní a jak odvozená rozhraní, ITimerLight tak IBlinkingLighti . V rozhraní je deklarována ILight pouze jedna "nejbližší" implementace. Každá třída, která deklarovala přepsání, se stane jednou "nejbližší" implementací. Viděli jste příklady v předchozích třídách, které přerodily členy jiných odvozených rozhraní.

Vyhněte se přepsání stejné metody v několika odvozených rozhraních. Tím se vytvoří nejednoznačné volání metody pokaždé, když třída implementuje obě odvozená rozhraní. Kompilátor nemůže vybrat jednu lepší metodu, takže vydá chybu. Pokud by například IBlinkingLightITimerLight i implementované přepsání PowerStatusbylo nutné OverheadLight zadat konkrétnější přepsání. Jinak kompilátor nemůže vybrat mezi implementacemi ve dvou odvozených rozhraních. Této situaci se obvykle můžete vyhnout tím, že definice rozhraní budou malé a zaměřené na jednu funkci. V tomto scénáři je každá schopnost světla vlastním rozhraním; pouze třídy dědí více rozhraní.

Tato ukázka ukazuje jeden scénář, ve kterém můžete definovat samostatné funkce, které se dají kombinovat do tříd. Deklarujete jakoukoli sadu podporovaných funkcí tím, že deklarujete, která rozhraní třída podporuje. Použití virtuálních výchozích metod rozhraní umožňuje třídám použít nebo definovat jinou implementaci pro jakoukoli nebo všechny metody rozhraní. Tato funkce jazyka poskytuje nové způsoby modelování skutečných systémů, které vytváříte. Výchozí metody rozhraní poskytují jasnější způsob vyjádření souvisejících tříd, které mohou kombinovat a odpovídat různým funkcím pomocí virtuálních implementací těchto funkcí.