Sign in
Log inSign up
What are default interface methods in C# 8.0?

What are default interface methods in C# 8.0?

Miguel Bernard's photo
Miguel Bernard
·Mar 29, 2020

Also in this series

  1. C# 8.0 Nullable Reference types are here!
  2. Pattern matching in C#
  3. Asynchronous streams
  4. Indices and ranges
  5. Default interface methods
  6. 5 tips to improve your productivity in C# 8.0

All code samples are available on github

Introduction

Last September, Microsoft released a bunch of new features with C# 8.0. One of the major features that they introduced is Default interface methods. In this post, we'll explain what they are, show a few examples and provide guidance to avoid common pitfalls.

What are default interface methods in C# 8.0

Interfaces in C# used to be definitions or contracts without any behavior. This assumption is no longer true. Now, with default interface methods, you can define a default body for any interface methods. When you do so, they mostly behave like virtual methods in an abstract class.

Why are default interface methods important

If they behave like virtual methods in an abstract class, why not simply use an abstract class? There is one big difference that justifies their existence:

In C#, you can only inherit from one base class, but you can implement as many interfaces as you want.

That subtle point makes all the difference when it comes to code reuse, a.k.a. the Don't Repeat Yourself (DRY) principle. That new feature will enable many scenarios that were previously difficult or impossible to achieve.

e.g.

Real examples of default interface methods

Without default interface methods

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

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

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

    public void WriteError(string message)
    {
        this.WriteCore(LogLevel.Error, message);
    }
}
internal 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;
        }
    }

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

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

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

For every implementation of ILogger we have to repeat this same code block:

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

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

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

With default interface methods

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

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

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

    void WriteError(string message)
    {
        this.WriteCore(LogLevel.Error, message);
    }
}
internal class ConsoleLogger : ILogger
{
    public void WriteCore(LogLevel level, string message)
    {
        Console.WriteLine($"{level}: {message}");
    }
}
internal 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;
        }
    }
}

Haaaaa... this is much better. Now the implementations only contain the relevant code.

Pitfall to avoid

As much as multiple inheritance is awesome, you also need to be very mindful of the dreaded diamond problem.

Diamond_inheritance-1

It's an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C?

Closing

There, you have another great tool to put in your toolbox to keep your code clean and maintainable. This approach is also very non-intrusive and won't conflict with existing code, which means it will be easy to slowly adopt it. If you want to hear more on this subject, let me know in the comments.

References

Also in this series

  1. C# 8.0 Nullable Reference types are here!
  2. Pattern matching in C#
  3. Asynchronous streams
  4. Indices and ranges
  5. Default interface methods
  6. 5 tips to improve your productivity in C# 8.0

All code samples are available on github