Вызов метода по умолчанию интерфейса C# из реализации класса

C# 8 поддерживает реализации методов по умолчанию в интерфейсах. Моя идея заключалась в том, чтобы внедрить метод ведения журнала в такие классы:

public interface ILoggable {
    void Log(string message) => DoSomethingWith(message);
}

public class MyClass : ILoggable {
    void MyMethod() {
        Log("Using injected logging"); // COMPILER ERROR
    }
}

Я получаю сообщение об ошибке компилятора: "Имя не существует в текущем контексте"

Невозможно ли таким образом использовать реализации методов по умолчанию?

РЕДАКТИРОВАТЬ:

Правильный ответ относительно правил C# см. В принятом ответе. Для более лаконичного решения (исходная идея моего вопроса!) См. Мой собственный ответ ниже.

8 ответов

Решение

См. Документацию по адресу https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/default-interface-members-versions.

Это бросило из SampleCustomer к ICustomerявляется необходимым. ВSampleCustomer класс не должен предоставлять реализацию для ComputeLoyaltyDiscount; это предусмотреноICustomerинтерфейс. Тем не менееSampleCustomerкласс не наследует члены от своих интерфейсов. Это правило не изменилось. Чтобы вызвать любой метод, объявленный и реализованный в интерфейсе, переменная должна быть типом интерфейса,ICustomer в этом примере.

Итак, метод похож на

public class MyClass : ILoggable {
    void MyMethod() {
        ILoggable loggable = this;
        loggable.Log("Using injected logging");
    }
}

Если вы хотите избежать беспорядка и повторяющегося приведения типов, вы можете добавить одно свойство, которое приводит тип как интерфейс:

public class MyClass : ILoggable 
{
    ILoggable AsILoggable => (ILoggable)this;

    void MyMethod() 
    {
        AsILoggable.Log("Using injected logging"); 
    }
}

Но это не так. Это кажется неправильным, независимо от того, как это делается. Из документации:

Наиболее распространенный сценарий - безопасное добавление членов в интерфейс, уже выпущенный и используемый бесчисленным количеством клиентов.

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

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

Если мы уже изменяем класс, почему бы просто не реализовать метод?

public void Log(string message) => DoSomethingWith(message);

Когда мы добавляем реализацию интерфейса по умолчанию, мы предоставляем реализацию потребителям интерфейса - классам, которые зависят от абстракции.

Если мы зависим от реализации интерфейса по умолчанию из в классе, который реализует интерфейс, то изменение интерфейса становится, по сути, изменение внутренней реализации класса. Интерфейс не для этого. Интерфейс представляет собой внешнее поведение, а не внутреннюю реализацию.

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

Я не буду заходить так далеко, чтобы сказать, что это неправильно, но это чувствует, как злоупотребление функции.

В CLR все реализации членов интерфейса являются явными, поэтому в вашем коде Log будут доступны в экземплярах ILoggableтолько, как это рекомендуется делать здесь:

((ILoggable)this).Log("Using injected logging")

Проблема с ответами, которые приводят класс к интерфейсу, заключается в том, что он может или не может вызывать метод интерфейса по умолчанию, в зависимости от того, реализовал ли класс метод для переопределения метода по умолчанию.

Итак, этот код:

      ((ILoggable)this).Log(...)

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

Если в классе есть метод, который переопределяет метод по умолчанию, то будет вызван именно этот метод. Обычно это желаемое поведение. Но если вы всегда хотите вызывать метод по умолчанию, независимо от того, реализовал ли реализующий класс свою собственную версию этого метода интерфейса, у вас есть несколько вариантов. Один из способов:

  1. Объявите метод по умолчанию как статический. Не волнуйтесь, вы все равно сможете переопределить его в классе, наследующем от него.
  2. Вызовите метод по умолчанию, используя тот же тип синтаксиса, при вызове статического метода класса, только замените имя интерфейса на имя класса.

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

Прочитав статью об этих методах по умолчанию, я думаю, вам следует попробовать преобразовать ее в интерфейс:

((ILoggable)this).Log("Using injected logging")

Не проверял, просто подумал по этой статье

Вот два альтернативных решения уже предложенным:

Во-первых, просто реализовать метод интерфейса:

      public class MyClass : ILoggable {
    void MyMethod() {
        Log("Using injected logging");
    }

    public void Log(string message) => ((ILog)this).Log(message);
}

Это позволяет вызывать метод напрямую, без необходимости каждый раз записывать приведение.

На заметку:

  • это сделает метод доступным для внешних пользователей, тогда как раньше он был доступен только тогда, когда экземпляр был приведен в / использовался как
  • если вы хотите использовать 10 различных методов из ILog в своем классе вы, вероятно, не захотите реализовывать их все.
  • с другой стороны, существует множество сценариев, в которых это «естественный» / предполагаемый подход, в первую очередь, когда расширяется метод интерфейса с помощью некоторой настраиваемой логики (например, ((ILog)this).Log("(MyClass): " + message) )

Во-вторых, используются методы расширения:

      public static class LogExtensions
{
  public static void Log<T>(this T logger, string message) where T : ILoggable => logger.Log(message);
}

public class MyClass : ILoggable {
    void MyMethod() {
        this.Log("Using injected logging");
    }
}

Это может быть полезно, когда ILoggable содержит много методов / реализован во многих классах.

  • это все еще позволяет Log быть перезаписанным в MyClass и переопределение, которое будет вызываться
  • по сути, просто синтаксический сахар, сокращение ((ILoggable)this) к this

Принятый ответ и другие ответы верны. Однако то, что я хотел, - это краткий вызовLogметод. Я добился этого с помощью метода расширения наILoggable интерфейс:

public static class ILoggableUtils { // For extension methods on ILoggable
    public static void Log(this ILoggable instance, string message) {
         DoSomethingWith(message, instance.SomePropertyOfILoggable);
    }
}

Таким образом, я могу хотя бы позвонить this.Log(...); в моем классе вместо уродливого ((ILoggable)this).Log(...).

Мое решение добавляет новый абстрактный класс между интерфейсом и его реализациями:

public interface ILoggable {
    void Log(string message);
    void SomeOtherInterfaceMethod();
}

public abstract class Loggable : ILoggable  {
    void Log(string message) => DoSomethingWith(message);
    public abstract void SomeOtherInterfaceMethod(); // Still not implemented
}

public class MyClass : Loggable {
    void MyMethod() {
        Log("Using injected logging"); // No ERROR
    }

    public override void SomeOtherInterfaceMethod(){ // override modifier needed
        // implementation
    };
}
Другие вопросы по тегам