Skip to main content

C# Default Interface Methods

Introduction

Prior to C# 8.0, interfaces in C# were purely contracts that defined methods, properties, events, or indexers that implementing classes or structs must provide. There was no way to specify a default implementation in the interface itself. However, with the introduction of C# 8.0, interfaces can now include default implementations for their members, known as default interface methods.

This feature allows interface designers to add new members to existing interfaces without breaking compatibility with classes that already implement those interfaces. It also enables multiple inheritance of behavior, which was previously impossible in C#.

Understanding Default Interface Methods

Default interface methods are methods defined in an interface that include an implementation. Classes and structs that implement these interfaces can either use the default implementation or override it with their own implementation.

Basic Syntax

Here's how you define a default interface method:

csharp
public interface ILogger
{
// Regular interface method declaration (no implementation)
void LogError(string message);

// Default implementation provided
void LogInfo(string message)
{
Console.WriteLine($"INFO: {message}");
}
}

How Default Interface Methods Work

When a class implements an interface with default methods, it can:

  1. Use the default implementation by not providing its own implementation
  2. Override the default implementation by providing its own method

Let's look at a simple example:

csharp
// Interface with default method
public interface IGreeter
{
void SayHello()
{
Console.WriteLine("Hello, World!");
}
}

// Class using the default implementation
public class DefaultGreeter : IGreeter
{
// No implementation for SayHello - will use the default
}

// Class overriding the default implementation
public class CustomGreeter : IGreeter
{
public void SayHello()
{
Console.WriteLine("Greetings, Universe!");
}
}

Here's how to use these classes:

csharp
// Usage example
public static void Main()
{
DefaultGreeter defaultGreeter = new DefaultGreeter();
CustomGreeter customGreeter = new CustomGreeter();

// Calls the default implementation from the interface
defaultGreeter.SayHello(); // Output: "Hello, World!"

// Calls the overridden implementation
customGreeter.SayHello(); // Output: "Greetings, Universe!"
}

Accessing Default Implementation Explicitly

To directly access the interface's default implementation, even if it's been overridden, you can use explicit interface implementation:

csharp
public class AdvancedGreeter : IGreeter
{
public void SayHello()
{
Console.WriteLine("Advanced greeting!");
}

public void UseDefaultGreeting()
{
// Access the default implementation explicitly
((IGreeter)this).SayHello();
}
}

Usage:

csharp
var greeter = new AdvancedGreeter();
greeter.SayHello(); // Output: "Advanced greeting!"
greeter.UseDefaultGreeting(); // Output: "Hello, World!"

Use Cases for Default Interface Methods

1. Evolving Interfaces Without Breaking Changes

One of the primary use cases for default interface methods is adding new functionality to existing interfaces without breaking code that already implements those interfaces.

csharp
// Original interface
public interface IDataProcessor
{
void ProcessData(string data);
}

// Updated interface with backward compatibility
public interface IDataProcessor
{
void ProcessData(string data);

// New method with default implementation
void ValidateData(string data)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentNullException(nameof(data));
}
}
}

2. Implementing the Traits Pattern

Default interface methods allow for a form of the "traits" or "mixins" pattern:

csharp
public interface ILoggable
{
void Log(string message)
{
Console.WriteLine($"LOG: {message}");
}

void LogError(string error)
{
Console.WriteLine($"ERROR: {error}");
}
}

public interface ISerializable
{
string Serialize()
{
return "Default serialization";
}
}

// Class can "inherit" behavior from multiple interfaces
public class DataService : ILoggable, ISerializable
{
public void ProcessRequest()
{
Log("Processing request...");
var data = Serialize();
Log($"Request serialized: {data}");
}
}

3. Implementing the Template Method Pattern

Default interface methods can also be used to implement the template method pattern:

csharp
public interface IDataProcessor
{
// Template method with default implementation
void Process(string data)
{
Validate(data);
var processed = Transform(data);
Save(processed);
Notify($"Processed: {processed}");
}

// Default implementation
void Validate(string data)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentNullException(nameof(data));
}
}

// Abstract method - must be implemented by classes
string Transform(string data);

// Default implementation
void Save(string data)
{
Console.WriteLine($"Saving: {data}");
}

// Default implementation
void Notify(string message)
{
Console.WriteLine(message);
}
}

// Implementation only needs to provide the required methods
public class SimpleProcessor : IDataProcessor
{
public string Transform(string data)
{
return data.ToUpper();
}
}

Practical Example: Building a Notification System

Let's see a real-world example of using default interface methods to create a flexible notification system:

csharp
public interface INotificationChannel
{
void Send(string message);

// Default methods
bool ValidateMessage(string message)
{
return !string.IsNullOrEmpty(message);
}

void SendWithValidation(string message)
{
if (ValidateMessage(message))
{
Send(message);
}
else
{
Console.WriteLine("Message validation failed");
}
}

void SendUrgent(string message)
{
Send("URGENT: " + message);
}
}

// Implementation for email notifications
public class EmailNotification : INotificationChannel
{
public void Send(string message)
{
Console.WriteLine($"Sending email: {message}");
}

// Custom validation for email
public bool ValidateMessage(string message)
{
return !string.IsNullOrEmpty(message) && message.Length <= 1000;
}
}

// Implementation for SMS notifications
public class SmsNotification : INotificationChannel
{
public void Send(string message)
{
Console.WriteLine($"Sending SMS: {message}");
}

// SMS uses the default validation
}

// Usage
public class NotificationExample
{
public static void Main()
{
INotificationChannel email = new EmailNotification();
INotificationChannel sms = new SmsNotification();

email.SendWithValidation("Hello via email"); // Uses custom validation
sms.SendWithValidation("Hello via SMS"); // Uses default validation

email.SendUrgent("Server is down!"); // Uses default urgent method
}
}

Output:

Sending email: Hello via email
Sending SMS: Hello via SMS
Sending email: URGENT: Server is down!

Limitations and Considerations

When using default interface methods, keep the following in mind:

  1. Accessibility Modifiers: Default interface methods can use the public, internal, and private modifiers. Private members can only be called by other code in the interface.

  2. No Fields: Interfaces still cannot contain instance fields.

  3. Version Conflicts: Be cautious of conflicts if a class implements multiple interfaces that provide default implementations for methods with the same name.

  4. Runtime Support: Default interface methods require runtime support. Ensure your target platform supports this feature.

  5. Diamond Problem: C# handles the "diamond problem" (when a class inherits from multiple interfaces with the same default method) by requiring the implementing class to explicitly implement the method.

csharp
public interface IA
{
void Method() => Console.WriteLine("IA.Method");
}

public interface IB
{
void Method() => Console.WriteLine("IB.Method");
}

// Must explicitly implement at least one of the methods
public class MyClass : IA, IB
{
// Explicit implementation to resolve the ambiguity
void IA.Method() => Console.WriteLine("MyClass.IA.Method");
void IB.Method() => Console.WriteLine("MyClass.IB.Method");
}

Summary

Default interface methods are a powerful feature in C# that enable:

  • Adding functionality to existing interfaces without breaking compatibility
  • Providing reusable implementations across multiple classes
  • Supporting multiple inheritance of behavior
  • Implementing design patterns like traits and template methods more elegantly

By understanding and leveraging default interface methods, you can create more flexible, maintainable, and extensible code in your C# applications.

Additional Resources

Exercises

  1. Create an interface IShape with default methods for calculating area and perimeter, and implement it for different shapes.

  2. Design a logging system with an ILogger interface that provides default methods for different log levels.

  3. Modify an existing interface in a project to add new functionality using default interface methods without breaking existing implementations.

  4. Implement the observer pattern using default interface methods to provide standard notification behavior.

  5. Create a scenario that demonstrates resolving the diamond problem with default interface methods.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)