blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

Understanding C# 8 default interface methods

In this post I provide an introduction to default interface methods, how they work at a high level, and their typical uses. Finally, I discuss some of the sharp edges on the feature: things to watch out for, compiler errors you could run into, and caveats about where you can use them. In my next post, I discuss a use case where default interface methods were used to improve the performance of ASP.NET Core.

Understanding default interface methods

Default interface methods were introduced in C# 8, primarily as a way to make it easier to evolve an interface without breaking people that have implemented the interface. The feature allows you to provide a method body when you define a method on an interface, similar to how you might use an abstract class.

Default interface members also allow for easier interoperability with similar features that exist in Android and iOS. They also offer some of the characteristics of traits which are an approach for building systems that focuses on composability over inheritance.

It's easiest to understand the feature with an example, so I'm going to use a stripped down version of the scenario in the Microsoft documentation. Lets say you have defined the following interface in a library:

public interface ICustomer
{
    DateTime DateJoined { get; }
    string Name { get; }
}

You ship this interface as part of your functionality, and customers are using the interface.They have several classes that implement this interface, one of which is shown below:

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
    public Guid Id { get; } = id;
    public string Name { get; } = name;
    public DateTime DateJoined { get; } = dateJoined;
}

The code above uses primary constructors to reduce some of the constructor boilerplate;

All is well, until you realise that you need to evolve your interface.

Evolving the interface is a breaking change

Later on, you decide you need to add another method to your interface, GetLoyaltyDiscount() which returns the discount a given customer gets. Ultimately you want an interface that looks like this:

public interface ICustomer
{
    DateTime DateJoined { get; }
    string Name { get; }
    decimal GetLoyaltyDiscount(); // 👈 Adding this 
}

Unfortunately, making this change is a breaking change. If you made this change, then when consumers of your library updated, their code would no longer compile until they implemented the new method.

If you're following semantic versioning (semver)—and you probably should be if you're a library author—then you could make this change in a major version bump. But it's still undesirable. It adds friction to consumers of your library, adding a step they have to undertake just to update your library. If you can avoid breaking changes, then that's generally a good thing.

Aaron Stannard has a great series on "Professional Open Source", and how it relates to versioning. I particularly like his post on "practical" vs "strict" semver as it handles the complexities of breaking changes in popular open source libraries, and how to think about them.

Depending on your exact requirements, you may be able to write GetLoyaltyDiscount() simply as an extension method that operates on ICustomer. But if you need consumers to be able to override this functionality then you are kind of stuck. Which is where default interface methods come in.

Default interface methods to the rescue

Default interface methods enable you to make this change without breaking consumers of your library, by providing a concrete implementation for the method. For example:

public interface ICustomer
{
    DateTime DateJoined { get; }
    string Name { get; }
    // 👇 Provide a body for the new method
    decimal GetLoyaltyDiscount()
    {
        var twoYearsAgo = DateTime.UtcNow.AddYears(-2);
        if (DateJoined < twoYearsAgo)
        {
            // Customers who joined > 2 years ago get a 10% discount
            return 0.10m;
        }

        // Otherwise no discount
        return 0;
    }
}

Lets pause a moment to think about how weird this is.

We're defining an interface here, not a class or struct. Yet we have a method body.

Prior to C# 8, you couldn't do that, and what's more, it also didn't really make sense. interfaces were literally that: definitions of the API/interface a type must implement. If you wanted to provide a "default" implementation, then you had to use an abstract class instead, which limited you in other ways.

I compare default interface methods to abstract classes more shortly.

So lets say you add this default method to your interface, and ship it in a new version of the library. You don't need to make a major version bump because this is not a breaking change. The consumer's existing implementation (shown again below) is still valid, even though it doesn't define GetLoyaltyDiscount():

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
    public Guid Id { get; } = id;
    public string Name { get; } = name;
    public DateTime DateJoined { get; } = dateJoined;
    // GetLoyaltyDiscount() is not implemented
}

The consumer's code still compiles, and when the library needs to use the ICustom.GetLoyaltyDiscount() method, it uses the default implementation.

Later, if the consumer decides they do want to implement GetLoyaltyDiscount(), they can do so:

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
    public Guid Id { get; } = id;
    public string Name { get; } = name;
    public DateTime DateJoined { get; } = dateJoined;
    public decimal GetLoyaltyDiscount() => 0; // Never give a discount
}

So if you can implement a change to an interface as a default interface method, you may get the best of both worlds: an evolving interface which doesn't cause breaking changes.

The default interface methods feature enabled more than just the feature above. You can now even define private members on a field which can be used as part of the default interface methods implementation, as well as also defining static methods. The documentation shows an example of these features here

If this all sounds to good to be true, you won't be surprised that there are some sharp edges to watch out for when working with default methods. In the next section I discuss some of the things to think about and watch out for

Sharp edges to watch out for with default interface methods

This section discusses some of the things to consider when working with default interface methods.

Default interface methods are not inherited

Although interfaces that use default interface methods look a lot like abstract classes, they're fundamentally different. One big difference is that default interface methods are not inherited by implementers of the interface.

For example, imagine you are working with the SampleCustomer before you added a custom GetLoyaltyDiscount() method:

public class SampleCustomer(Guid id, string name, DateTime dateJoined) : ICustomer
{
    public Guid Id { get; } = id;
    public string Name { get; } = name;
    public DateTime DateJoined { get; } = dateJoined;
    // GetLoyaltyDiscount() is not implemented
}

The following code that uses the default interface implementation of GetLoyaltyDiscount() compiles, and prints 0.10:

// Create a customer that joined 3 years ago
SampleCustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));
PrintDiscount(customer); // Prints "0.10"

static void PrintDiscount(ICustomer customer)
  => Console.WriteLine(customer.GetLoyaltyDiscount()); // Use the default interface method

However, the following does not compile:

// Create a customer that joined 3 years ago
SampleCustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));

Console.WriteLine(customer.GetLoyaltyDiscount()); // 💥 The GetLoyaltyDiscount() method does not exist!

The problem is that the GetLoyaltyDiscount() method is not inherited by SampleCustomer. The method doesn't exist on SampleCustomer, so you can't call it.

However, the following, in which we cast the SampleCustomer to an ICustomer does compile and prints 0.10 as expected:

// Create a customer that joined 3 years ago
ICustomer customer = new SampleCustomer(Guid.Empty, "me", DateTime.Now.AddYears(-3));
// 👆 cast to ICustomer

Console.WriteLine(customer.GetLoyaltyDiscount()); // Prints 0.10

So the lesson here is that you may need to cast to ICustomer to call default interface methods. But things get even more confusing if you start adding more types into the hierarchy...

Understanding which method will be called is…complicated

OK, a quick puzzle., what does this program print?

using System;

// 👇 casting to IShape
IShape shape = new Square(); 
Console.WriteLine(shape.GetName()); // 👈 What does this print?

public interface IShape
{
    string GetName() => "IShape"; // Default implementation
}

public class Rectangle : IShape
{
}

public class Square : Rectangle
{
    public string GetName() => "Square"; // Specific implementation
}

The answer is that this prints IShape, even though Square has provided an overriding implementation for the GetName() method! The Rectangle type in the hierarchy implements IShape and it doesn't override the behaviour of GetName(), so the compiler uses the default implementation instead.

The best way to "fix" this behaviour is to make sure you explicitly implement the IShape interface on Square even though it's implemented implicitly by inheriting from Rectangle:

// Expicitly implement the interface 👇
public class Square : Rectangle, IShape
{
    public string GetName() => "Square"; // This now overrides the default implementation
}

Now if you run the program above the code prints Square, as you may have originally expected! IMO, this is one of the sharpest edges of default interface methods, as you don't really get any help from the compiler about this unintuitive behaviour.

The diamond inheritance problem

Thankfully, for some default interface method problems, the compiler does provide you some help. One such problem is the diamond problem, which is a well known problem associated with multiple inheritance.

The following example shows a simple demonstration of the problem:

interface IShape
{
    string GetName() => "IShape"; // Default implementation
}

interface IHasStraightEdges : IShape
{
    string IShape.GetName() => "IHasStraightEdges"; // Override the default
}

interface IHasCurvedEdges : IShape
{
    string IShape.GetName() => "IHasCurvedEdges"; // Override the default
}

// inherits both interfaces
public class MySemiCircle : IHasStraightEdges, IHasCurvedEdges {}

The above code won't compile, instead you get an error:

Interface member `IShape.GetName()` does not have a most specific
implementation. Neither `IHasStraightEdges.IShape.GetName()` or 
`IHasCurvedEdges.IShape.GetName()` is most specific.

It's easy to visualize the problem by thinking about what the following would print:

IShape shape = new MySemiCircle(); // cast to IShape
Console.WriteLine(shape.GetName()); // What should this print?!

The compiler doesn't have a "best" method to call, so it gives up. This is the classic diamond of death. The compiler forces you to specifically override the method, for example:

                     // Optionally Implement IShape explicitly  👇
public class MySemiCircle : IHasStraightEdges, IHasCurvedEdges, IShape
{
    public string GetName() => "MySemiCircle"; // 👈 Provide an implementation (required)
}

Structs implementing interfaces are problematic

Implementing an interface isn't limited to a class, you can implement it in a struct as well. Unfortunately, structs don't play nicely with default interface methods:

  • To use a default interface method you must cast to the interface, e.g. (IShape)myStruct. However, casting to an interface requires boxing the struct, allocating on the heap, and likely invalidating one of the main reasons structs are used in C# (to reduce allocations)
  • If the default interface method modifies the state of the object, it will be operating on the boxed copy of the struct, so modifications won't occur to the original. This is unlikely to be the desired behaviour.

Unfortunately, there's not much you can do about these limitations. A good rule of thumb is simply don't use a struct to implement interfaces that use default interface methods.

Of course, the whole point of default interface methods is that you start off without them, and then add them later, which means this problem could spring on you out of nowhere😬

Default interface methods require runtime support

Implementing default interface methods didn't just require changes to the C# compiler, it also required changes to the .NET runtime too. These changes were added to the CoreCLR (and mono) runtime in .NET Core 3.0 when C# 8 was released, so you can't use them in .NET Core 1.x or .NET Core 2.x (Yes, people are still using these 😅).

Matt Warren has a great post looking at how the feature was implemented in the runtime if you want to get into the weeds!

More importantly, you also can't use default interface members in .NET Framework. That means that if your open source project still targets .NET Framework you won't be able to use default interface members.

Summary

In this post I described the C# 8 default interface methods feature. This feature lets you enable interface implementations without breaking consumers of the interface, by allowing you to give a body to your interface members. This provides an entirely new mechanism for sharing functionality between multiple types, but it also can give some tricky edge-cases, because the default interface implementation is not inherited by implementers of the interface. In the next post, I discuss a use case where default interface methods were used to improve the performance of ASP.NET Core.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?