Also in this series
- C# 8.0 Nullable Reference types are here!
- Pattern matching in C#
- Asynchronous streams
- Indices and ranges
- Default interface methods
- 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.
- Extend interfaces safely by adding methods with implementations
- Create parameterized implementations to provide greater flexibility
- Enable implementers to provide a more specific implementation in the form of an override
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.
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
- C# 8.0 Nullable Reference types are here!
- Pattern matching in C#
- Asynchronous streams
- Indices and ranges
- Default interface methods
- 5 tips to improve your productivity in C# 8.0
All code samples are available on github