Руководство. Сочетание функциональных возможностей при создании классов с помощью интерфейсов с методами интерфейса по умолчанию

Вы можете определить реализацию при объявлении члена интерфейса. Эта функция предоставляет новые возможности, позволяющие определить реализации по умолчанию для компонентов, объявленных в интерфейсах. Классы могут выбирать, когда следует переопределять функциональность, когда следует использовать функциональную возможность по умолчанию и когда не следует объявлять поддержку отдельных функций.

Из этого руководства вы узнаете, как выполнять следующие задачи:

  • Создать интерфейсы с реализациями, описывающими отдельные функции.
  • Создать классы, которые используют реализации по умолчанию.
  • Создать классы, которые переопределяют некоторые или все реализации по умолчанию.

Необходимые компоненты

Необходимо настроить компьютер для запуска .NET, включая компилятор C#. Компилятор C# доступен в Visual Studio 2022 или пакете SDK для .NET.

Ограничения методов расширения

Одним из способов реализации поведения, проявляемого в рамках интерфейса, является определение методов расширения, которые обеспечивают поведение по умолчанию. Интерфейсы объявляют минимальный набор элементов, предоставляя большую контактную зону для любого класса, реализующего этот интерфейс. Например, методы расширения в 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")}";
}

В этом учебнике код не поддерживает устройства Интернета вещей, но эмулирует такие действия, записывая сообщения в консоль. Вы можете исследовать код без автоматизации дома.

Теперь определим интерфейс, автоматически отключающий освещение по истечении времени ожидания.

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

В отличие от переопределения методов виртуального TurnOnFor класса, объявление в HalogenLight классе не использует 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 потребуется предоставить более конкретное переопределение. В противном случае компилятор не сможет выбрать между реализациями двух производных интерфейсов. Обычно эту ситуацию можно избежать, сохраняя определения интерфейсов компактными и направленными на одну функцию. В этом сценарии каждая возможность света является собственным интерфейсом; только классы наследуют несколько интерфейсов.

В этом примере показан один из сценариев, в котором можно определить отдельные функции, которые можно комбинировать в классы. Вы объявили любой набор поддерживаемых функциональных возможностей, объявляя интерфейсы, поддерживаемые классом. Виртуальные методы интерфейса по умолчанию позволяют классам использовать или определять различные реализации для любого или всех методов интерфейса. Такая возможность языка предоставляет новые способы для моделирования реальных систем, которые вы создаете. Методы интерфейса по умолчанию предоставляют более понятный способ для выражения связанных классов, которые могут смешивать и сопоставлять различные возможности с помощью их виртуальных реализаций.