BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Default Interface Methods in C# 8

Default Interface Methods in C# 8

This item in japanese

Bookmarks

Key Takeaways

  • Default interface methods are included in a new feature proposal for C# 8, which will allow developers to use the traits programming technique.
  • Traits are object-oriented programming technology that promotes the reuse of methods between unrelated classes.
  • The C# language developers have based this feature on Java's Default Methods  concept.
  • C# Addresses the diamond inheritance problem that can occur with default interface methods by taking the most specific override at runtime.
  • C# Compiler will try to protect you from making many common implementation errors when utilizing default interface methods.

Default interface methods (also known as virtual extension methods) is a new feature proposal for C# 8, which will allow C# developers to use the Traits programming technique. Traits are object-oriented programming technology that promotes the reuse of methods between unrelated classes.

In this article, I will explain the most important things about this new feature, including the new C# Syntax, and I will show an example of how this feature can make your code more clean and compact.

The main benefit that default methods bring is that now it is possible to add a new default method to an existing interface without breaking the classes that implement that interface. In other words, this feature makes it optional for implementers to override the method or not.

An excellent scenario for this feature is the logging example that is described below; the ILogger interface has one abstract WriteLogCore method. All of the other methods, like WriteError and WriteInformation, are default methods that call the WriteLogCore method with a different configuration. The ILogger implementer has to implement only the WriteLogCore method.

Think of how many lines of code that you have saved in each inherited class of the logger type. While this feature can be a great thing, there are some dangers as it is a form of multiple inheritances.  Hence it suffers from the Diamond Problem, which is described below. Also, the interface methods must be "pure behavior" without the state; that means the interfaces, still, as in the past, cannot directly reference a field.

The C# syntax for an interface in .NET compiler is extended to accept the new keywords in the interfaces which are listed below.  For example, you can write a private method in the interface and the code will still compile and work.

  • A body for a method or indexer, property, or event accessor
  • Private, protected, internal, public, virtual, abstract, override, sealed, static, extern
  • Static fields
  • Static methods, properties, indexers, and events.
  • Explicit access modifiers with default access is public
  • Override modifiers

Not allowed:

  • Instance state, instance fields, instance auto-properties

Default Interface Method Example

Consider this simple example that illustrates how this feature works.

// ------------------------Default Interface Methods---------------------------

  interface IDefaultInterfaceMethod
  {
    public void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }
  }

  class AnyClass : IDefaultInterfaceMethod
  {
  }

  class Program
  {
    static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();
    }
  }

Console Output:

> I am a default method in the interface!

If you look at the code above, you can see that the interface has a default method, and the implementer class contains neither any knowledge of this default method nor an implementation of that interface method.

Change the IDefaultInterfaceMethod to AnyClass, as shown below:

AnyClass anyClass = new AnyClass();
  anyClass.DefaultMethod();

The above code will produce the compile-time error: AnyClass does not contain a member DefaultMethod.

That is the proof that the inherited class does not know anything about the default method.

[Click on the image to enlarge it]

Figure -1- Call by the class error message

To access the default method of the interface, you must upcast it to the interface:

AnyClass anyClass = new AnyClass();
  ((IDefaultInterfaceMethod)anyClass).DefaultMethod(); 

Console Output:

> I am a default method in the interface!

It is also worth mentioning that the same feature has existed for a long time in Java and the .NET team has considered the Java Default Methods documentation as a reference for the .NET Framework Developers, for example:  

"We should look more deeply into what Java does here. There must be accumulated insight already on this topic." - C# Language Design Notes for Apr 11, 2017

Modifiers in Interfaces

As I mentioned before, the C# syntax for an interface is extended to accept the following keywords:  protected, internal, public and virtual. By default, the default interface methods are virtual unless the sealed or private modifier is used. Similarly, abstract is the default on interface members without bodies.

Example:

  // ------------------------ Virtual and Abstract---------------------------

interface IDefaultInterfaceMethod
  {
    // By default, this method will be virtual, and the virtual keyword can be here used!
    virtual void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }

    // By default, this method will be abstract, and the abstract keyword can be here used
    abstract void Sum();
  }

  interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method!");
    }
  }

  class AnyClass : IDefaultInterfaceMethod, IOverrideDefaultInterfaceMethod
  {
    public void Sum()
    {
    }
  }

  class Program
  {
      static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();

      IOverrideDefaultInterfaceMethod anyClassOverridden = new AnyClass();
      anyClassOverridden.DefaultMethod();
    }
  }

Console output:

> I am a default method in the interface!
> I am an overridden default method!

The keywords virtual and abstract can be removed from the interface; however, removing them does not have any effect on the compiled code.

Note: No access modifier is permitted in the overridden method.

Override example:

interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    public void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method");
    }
  }

The above code will produce the compile-time error: The modifier ‘public’ is not valid for this item.

[Click on the image to enlarge it]

Figure -2- Modifiers are not allowed in the overridden method

Diamond Problem

An ambiguity that can arise because of allowing multiple inheritance. It is a big problem for languages (like C++) that allow multiple inheritance of state. In C#, however, multiple inheritance is not allowed for classes, but rather only for interfaces in a limited way, so that does not contain state.

Figure -3- Diamond Problem dependencies

Consider the following situation:

// ------------------------Diamond inheritance and classes---------------------------

  interface A
  {
    void m();
  }

  interface B : A
  {
    void A.m() { System.Console.WriteLine("interface B"); }
  }

  interface C : A
  {
    void A.m() { System.Console.WriteLine("interface C"); }
  }

  class D : B, C
  {
    static void Main()
    {
      C c = new D();
      c.m();
    }
  }

The above code will produce the compile-time error, which shown below in Figure -4-:

[Click on the image to enlarge it]

Figure -4- Diamond Problem error message

The .NET developer team has decided to solve the Diamond Problem by taking the most specific override at runtime.

"Diamonds with Classes
A class implementation of an interface member should always win over a default implementation in an interface, even if it is inherited from a base class. Default implementations are always a fallback only for when the class does not have any implementation of that member at all."

If you want to know more about this problem, you can find more information in the Proposal: Default Interface Methods and C# Language Design Notes for Apr 19, 2017

Back to our example, the problem is that the most specific override cannot be inferred from the compiler. However, you can add the method ‘m’ in the class ‘D’ as shown below, and now the compiler uses the class implementation to solve the diamond problem.

  class D : B, C
  {
    // Now the compiler will use the most specific override, which is defined in the class ‘D’
    void A.m()
    {
       System.Console.WriteLine("I am in class D"); 
    }

    static void Main()
    {
      A a = new D();
      a.m();
    }
  }

Console Output:

> I am in class D


This Keyword
The code below is an example which shows how to use ‘this’ keyword in the interfaces.

public interface IDefaultInterfaceWithThis
  {
    internal int this[int x]
    {
      get
      {
        System.Console.WriteLine(x);
        return x;
      }
      set
      {
        System.Console.WriteLine("SetX");
      }
    }

    void CallDefaultThis(int x)
    {
      this[0] = x;
    }
  }

  class DefaultMethodWithThis : IDefaultInterfaceWithThis
  {
  }

Using the code:

  IDefaultInterfaceWithThis defaultMethodWithThis = new DefaultMethodWithThis();
  Console.WriteLine(defaultMethodWithThis[0]);  
  defaultMethodWithThis.CallDefaultThis(0); 

Console Output:

0
SetX

The ILogger Example

The logger interface is the most used example to explain the Default methods technique. In my code example below which contains one abstract method named "WriteCore". The other methods have a default implementation. ConsoleLogger and TraceLogger implementing the interface. If you look at the code below, you can see that the code is compact and clean. In the past, it was mandatory to implement the methods in a class that implements an interface unless that class is an Abstract class, and this might make your code DRY.  With the new approach, the class ConsoleLogger will be able to inherit from another class hierarchy, in other words, using default methods will give you the most flexible design.

  enum LogLevel
  {
    Information,
    Warning,
    Error
  }

  interface ILogger
  {
    void WriteCore(LogLevel level, string message);

    void WriteInformation(string message)
    {
      WriteCore(LogLevel.Information, message);
    }

    void WriteWarning(string message)
    {
      WriteCore(LogLevel.Warning, message);
    }

    void WriteError(string message)
    {
      WriteCore(LogLevel.Error, message);
    }
  }

  class ConsoleLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      Console.WriteLine($"{level}: {message}");
    }
  }

  class TraceLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      switch (level)
      {
        case LogLevel.Information:
          Trace.TraceInformation(message);
          break;

        case LogLevel.Warning:
          Trace.TraceWarning(message);
          break;

        case LogLevel.Error:
          Trace.TraceError(message);
          break;
      }
    }
  }

Using the code:

      ILogger consoleLogger = new ConsoleLogger();
      consoleLogger.WriteWarning("Cool no code duplication!");  // Output: Warning: Cool no Code duplication!

      ILogger traceLogger = new TraceLogger();
      consoleLogger.WriteInformation("Cool no code duplication!");  // Cool no Code duplication!

The Player Example

A game, which contains different types of players. The power player causes the most attack damage, on the other side, the limited player can cause less damage.  

  public interface IPlayer
  {
    int Attack(int amount);
  }

  public interface IPowerPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 50;
    }
  }

  public interface ILimitedPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 10;
    }
  }

  public class WeakPlayer : ILimitedPlayer
  {
  }

  public class StrongPlayer : IPowerPlayer
  {
  }

Using the code:

  IPlayer powerPlayer = new StrongPlayer();         
  Console.WriteLine(powerPlayer.Attack(5));  // Output 55

  IPlayer limitedPlayer = new WakePlayer();
  Console.WriteLine(limitedPlayer.Attack(5));  // Output 15

As you see in the code example above, the default implementation is in the IPowerPlayer interface, and in the ILimitedPlayer interface. The limited player doing less damage. If we define a new class, for example, SuperDuperPlayer, which inherited from the class StrongPlayer, then the new class automatically receives the default power attack behavior from the interface, as shown in the example below.

  public class SuperDuperPlayer: StrongPlayer
  {
  }

  IPlayer superDuperPlayer = new SuperDuperPlayer();
  Console.WriteLine(superDuperPlayer.Attack(5)); // Output 55

The Generic Filter Example

ApplyFilter is a default interface method that contains a predicate which is applied to a generic type. In my example, the dummy filter is used to simulate mocked behavior. 

  interface IGenericFilter<T>
  {
    IEnumerable<T> ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      foreach (var item in collection)
      {
        if (predicate(item))
        {
          yield return item;
        }
      }
    }
  }

  interface IDummyFilter<T> : IGenericFilter<T>
  {
    IEnumerable<T> IGenericFilter<T>.ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      return default;
    }
  }

  public class GenericFilterExample: IGenericFilter<int>, IDummyFilter<int>
  {
  }

Using the code:

      IGenericFilter<int> genericFilter = new GenericFilterExample();
      var result = genericFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

Console output:

2, 3

      IDummyFilter<int> dummyFilter = new GenericFilterExample();
      var emptyResult = dummyFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);

Console output:

0

You can use this generic filter concept for many other designs and needs.

Limitations

There are some limitations and considerations that you first need to understand when using modifier keywords in the interfaces. In many cases, the compiler protects you against your mistakes by detecting a lot of common errors such as those listed below.

Consider the following Code:

  interface IAbstractInterface
  {
    abstract void M1() { }
    abstract private void M2() { }
    abstract static void M3() { }
    static extern void M4() { }
  }

  class TestMe : IAbstractInterface
  {
    void IAbstractInterface.M1() { }
    void IAbstractInterface.M2() { }
    void IAbstractInterface.M3() { }
    void IAbstractInterface.M4() { }
  }

The above code will produce the below listed compile-time errors:

error CS0500: 'IAbstractInterface.M1()' cannot declare a body because it is marked abstract
error CS0621: 'IAbstractInterface.M2()': virtual or abstract members cannot be private
error CS0112: A static member 'IAbstractInterface.M3()' cannot be marked as override, virtual, or abstract
error CS0179: 'IAbstractInterface.M4()' cannot be extern and declare a body
error CS0122: 'IAbstractInterface.M2()' is inaccessible due to its protection level

error CS0500, means the default method 'IAbstractInterface.M3()' cannot be abstract and at the same time, it has a body. error CS0621, means the method cannot be private and at the same time abstract.

In Visual Studio:

[Click on the image to enlarge it]

Figure -5- Compilation Errors in Visual Studio

Source code repositories and other developer resources

More information and source code:

About the Author

Bassam Alugili is Senior Software Specialist and databases expert at STRATEC AG. STRATEC is a world-leading partner for fully automated analyzer systems, software for laboratory data management, and smart consumables.

 

Rate this Article

Adoption
Style

BT