1. From "pure contract" to flexible architecture
Before C# 8, an interface was like a strict contract: if you wanted to implement an interface, you had to implement everything, down to the last comma. If new members appeared, all existing implementations had to urgently add them, or else the compiler wouldn't let you build the project.
But life is messier. Imagine you're maintaining a library used by hundreds of projects, and suddenly you need to add a new method to an interface. Don't want to break backward compatibility? That's where default methods (Default Interface Methods, DIM) come in!
What's the deal?
Default methods let you declare a method implementation right in the interface. Now the contract is more flexible: if a class doesn't implement the "new thing," the default implementation will be used. It's like a stunt double in a movie: if the actor doesn't want to jump off a bridge, the stuntman steps in.
2. Syntax of Default Interface Methods
How do you declare methods with implementation in an interface?
It's a lot like regular methods, except now you can (and should) write the method body right in the interface:
public interface ILogger
{
void Log(string message);
// New method with a default implementation!
void LogWarning(string message)
{
Log("[WARNING] " + message);
}
}
Here, LogWarning already has an implementation! Any class that implements ILogger only has to implement Log, and LogWarning will get the default implementation (unless it has its own).
Compare: classic vs. modern signature
| Version | Declaration in interface |
|---|---|
| Before C# 8 | |
| C# 8 and newer | |
Important syntax details
- For a method with an implementation, you must write the method body in curly braces.
- Default methods can't be abstract.
- All interface methods are still implicitly public.
- You can also declare properties with default get/set (see below).
3. Practical examples
Example 1. Keeping backward compatibility
Let's say your app has an interface for saving data:
public interface ISaveable
{
void Save(string filePath);
}
Later, you decide to add cloud saving. Don't want to change a hundred classes? Just add a default method!
public interface ISaveable
{
void Save(string filePath);
// New method with a "default" implementation!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"Saving to cloud {cloudService} (by default — does nothing)");
}
}
Now all the old classes automatically "know how" to save to the cloud (even if for now it just prints a message).
Example 2. Expanding the logger interface
Earlier, we had a simple logging interface:
public interface ILogger
{
void Log(string message);
}
Let's add a default method for logging errors:
public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log("[ERROR] " + message);
}
}
A class implementing ILogger doesn't have to implement LogError — the default version will work:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// Not implementing LogError — default implementation will be used!
}
ILogger logger = new ConsoleLogger();
logger.Log("Everything's fine!");
logger.LogError("Uh-oh, something went wrong!"); // Calls the default implementation
Example 3. Default methods + extensible app
Your app supports different export types: to file, to DB, to network. The interface:
public interface IExporter
{
void Export(string data, string destination);
// New feature — export to archive
void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("Archiving is not supported by default.");
}
}
Plugins written by other devs will keep working, even if they know nothing about the new method.
4. How does calling default interface methods work?
Scenario: "old class — new interface"
If a class doesn't implement a default interface method, when you call it through an interface reference, the interface implementation is used. If it does implement it — its own version is used.
public class FileExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("Saving to file...");
}
// Not implementing ExportToArchive — default output will be used
}
IExporter exporter = new FileExporter();
exporter.Export("data", "file.txt"); // FileExporter's implementation runs
exporter.ExportToArchive("data", "file.zip"); // Default implementation runs!
Scenario: "class overrides default method"
public class AdvancedExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("Saving in advanced mode...");
}
public void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("Archiving is supported!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("data", "file.zip"); // Now the class implementation is called!
5. What else can you do with Default Interface Methods?
Default properties and events
You can declare properties with a default implementation, if they have a get or set body:
public interface IHasId
{
// Automatically returns 42, unless overridden
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
Calling default methods from interface code
Inside an interface, default methods and other interface members can call each other:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Limitations and quirks of Default Interface Methods
Can you declare fields, constructors?
Nope. Even with default methods, an interface is still not a class. No fields, constructors, or destructors allowed.
Can you use base with interfaces?
You can, but with caveats. Inside a default interface method, you can call a parent interface method by specifying it explicitly:
public interface IBase
{
void Greet() => Console.WriteLine("Hello from IBase");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("Hello from IDerived!");
IBase.Greet(this); // Explicitly call the base interface method
}
}
But you rarely need this for basic scenarios.
What happens if default implementations conflict?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// Class doesn't explicitly implement Foo:
public class C : IA, IB { }
// Compile error: not clear which implementation to pick!
7. Typical mistakes, limitations, and quirks
Funny common mistake:
Some folks try to declare fields in an interface after learning about DIM — but you still can't have fields. Also, if you try to implement a static default method — before C# 8 that's impossible. (From version 8, you can have static methods in interfaces, but default implementations of static methods are a separate topic.)
Quirk: Diamond Problem
If your class implements two interfaces with the same default method, you have to explicitly implement that method:
public class ConflictClass : IA, IB
{
public void Foo() // you have to pick which one!
{
// Explicitly call the needed implementation via interface (if needed)
((IA)this).Foo();
// or
((IB)this).Foo();
}
}
Don't overdo it!
Default methods save backward compatibility, but if you overuse them, you can end up with a "messy" architecture where some logic is scattered across interfaces. Try to keep all the important stuff in classes, and use interfaces as a real "contract."
GO TO FULL VERSION