Code Inside an Interface: C# 8 Adds a New Way to Ruin Lives
We need to talk about default interface methods

One of the best features of C# is that it doesn’t stand still. Every release brings new features — and not just simple syntactic sugars. Over the years, C# has added generics, lambda expressions, async and await, nullable types, and LINQ, just to name a few of its evolutionary leaps. This openness to change is part of the reason for C#’s success compared to other .NET languages. Its one-time competitor, VB.NET, spent more time adding conveniences (like the reviled My object), while implementing only some of the new language features seen in C#. As a result, VB.NET gradually became a less dynamic and less current language.
C# 8 brings the usual buffet of interesting enhancements. But one feature has me wondering if Anders Hejlsberg and his team are just a tiny bit too optimistic about the good instincts of programmers. The feature is default interface methods, and to understand its rationale you need to know a little bit about the challenges of interfaces.
Object-oriented programming is known for a number of things, including two features that shape the way that entire systems interact: inheritance and interfaces. Some developers fall in love with one of these and promote it to the exclusion of the other. (I, myself, am in the interface camp. I think inheritance makes plenty of sense for framework builders, but tangling up business code with inheritance hierarchies is a nightmare. Just use composition!)
Interfaces are remarkably straightforward. They’re a contract that any class can promise to follow. If some piece of code hands you an object that implements an interface, you know at least something about what you can do with it, even if you understand nothing else about that object. That makes interfaces one of the purest expressions of abstraction, and abstraction is one of the greatest virtues of good code. So you can see why I love them.
// An interface that defines the members a class needs
// in order to be treated as a document
interface IDocument
{
string Title { get; set; }
byte[] Content { get; set; } void Open();
void Close();
void Save();
}But interfaces also have a versioning problem. Once you create an interface, you can’t change it — for the same reason that you can’t alter a contract after someone signs it. After all, other classes are depending on that interface to set the standards of their behavior, and if you change the interface they are no longer in compliance with it!
The solution is to avoid changing interfaces, and simply make new ones. In the ancient days of COM, this lead to nonsense like IDocument and IDocument2. Classes would now need to implement both the new and old interfaces. (One method could provide an implementation for two interfaces, so this design wasn’t too horrible, but the naming system was messy.)
In C# the situation isn’t as bad, because one interface can inherit from another. So if you want to add a Print() method to your IDocument, you can do this:
// An enhanced version of IDocument
interface IDocument2 : IDocument
{
void Print();
}C# 8 goes farther. It tries to improve on this situation by providing a mechanism that you can use to expand an existing interface. In other words, C# 8 lets you change an interface, without breaking the classes that implement that interface.
How does it work this magic? C# 8 allows the creator of an interface to supply a default implementation of a new method. This is a reasonable bit of code that will be used if the implementing class doesn’t offer its own version.
interface IDocument
{
// The old
string Title { get; set; }
byte[] Content { get; set; }
void Open();
void Close();
void Save(); // The new
void Print(IPrinter printer)
{
// If we don’t have explicit instructions for what to do
// to print this document, let's just get the content as a
// big unicode string.
stringRepresentation =
Encoding.UTF8.GetString(Content, 0, Content.Length); printer.Print(stringRepresentation);
}
}Notice that there’s no inheritance here — just one changed interface that has a new method with code inside it (shudder). If you think this looks a bit like a default method in an abstract class, you’d be right.
Writing a reasonable default implementation can be tricky, because you don’t have access to any of the state in the class. You can work with the public members of the interface and any parameters that are passed in to your method, but that’s all.
So what happens when you implement an interface that has a default method, like IDocument? You have a choice. You can use the existing implementation of the Print() method (much as you might do when inheriting from a class), or you can roll your own. Classes that already implement IDocument keep working — but at a cost. There’s no guarantee the default implementation will make sense for them.
This is the central danger of default implementations — it allows careless or hurried programmers to retrofit code with something that may not be appropriate. If your code receives an object that uses the IDocument class, you have no way to tell if this object is using the default fallback implementation or its own tailor-made version. This make sense from an encapsulation point of view. But it also means that you could unwittingly use an object in a way that it doesn’t really support. Worst of all, the implementor gets no say on whether the implementation makes sense. They may not even know that the interface changed and their class has a new member! In the worst case scenario, you end up trading compile-time problems for wonky runtime behavior.
It’s not like we haven’t been here before. Inheritance dazzled as a cool feature for code reuse, but the fragile base class problem became a cancer for plenty of programmers who chased architectural purity too far. Default implementations are great if used carefully and appropriately, but what happens when they face the wishful thinking of overly optimistic programmers? And does anyone believe a lazy coder won’t just hack up a quick default method and throw a NotImplementedException inside?
In truth, I’m a long-time lover of the next shiny new thing in any programming language. I’ll probably find reasons to use default implementations — compatibility with untouchable bits of legacy code, porting code from other languages that support similar mechanisms, like Java, and so on. But the idea of adding a back door for sneaking in interface changes gives me a tingle of anxiety, and I won’t be surprised if today’s new feature becomes tomorrow’s new headache.
For more programming news, sign up for the once-a-month Young Coder newsletter.
