TL;DR - Default Interface Methods vs Abstract Methods
- Use default interface methods for API evolution and optional behavior without breaking existing code.
- Use abstract methods and classes for shared state, dependency injection, and complex inheritance.
- Interfaces support multiple inheritance; abstract classes do not.
- Abstract classes allow fields, constructors, and access modifiers; interfaces do not.
- Performance is similar; choose based on design and maintainability.
Default interface methods arrived in C# 8, letting you add implementation directly to interfaces. But when should you use them over traditional abstract methods?
Key Differences
Feature | Default Interface Methods | Abstract Methods |
---|---|---|
Access Modifiers | Public only | Public, protected, private |
State Management | No instance fields | Full field access |
Constructor Logic | Not supported | Complete constructor support |
Inheritance | Interface inheritance | Class inheritance |
Multiple Inheritance | Yes (interfaces) | No (single class) |
Default Interface Methods: API Evolution
Perfect for evolving existing APIs without breaking implementations:
public interface IUserService
{
Task<User> GetUserAsync(int id);
// Added in v2.0 - doesn't break existing implementations
public async Task<User[]> GetActiveUsersAsync()
{
// Default implementation
return await GetUsersAsync().Where(u => u.IsActive).ToArrayAsync();
}
protected virtual Task<IQueryable<User>> GetUsersAsync() =>
throw new NotImplementedException("Override in implementation");
}
Abstract Methods: Shared State and Logic
Better for complex shared behavior with state management:
public abstract class ServiceBase
{
protected readonly ILogger _logger;
protected readonly IConfiguration _config;
protected ServiceBase(ILogger logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
protected virtual async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
var maxRetries = _config.GetValue<int>("MaxRetries");
for (int i = 0; i < maxRetries; i++)
{
try
{
return await operation();
}
catch (Exception ex) when (i < maxRetries - 1)
{
_logger.LogWarning("Retry {Attempt} failed: {Error}", i + 1, ex.Message);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
throw new InvalidOperationException("Max retries exceeded");
}
public abstract Task<string> ProcessAsync();
}
When to Choose What
Feature / Scenario | Default Interface Methods | Abstract Methods |
---|---|---|
API Evolution | Allows evolving APIs without breaking changes | Adding methods breaks implementations |
Multiple Inheritance | Supported (via interfaces) | Not supported (single class only) |
Optional Behavior Extensions | Can provide default implementations | Must be implemented in derived class |
State Management | No instance fields | Full field and state support |
Constructor Dependency Injection | Not supported | Supported |
Access Modifiers | Public only | Public, protected, private |
Complex Shared Logic | Limited | Supported |
Performance | Virtual dispatch (optimized) | Direct inheritance (optimized) |
Best Use Case | API evolution, optional features | Shared state, DI, complex logic |
Performance Impact
Both approaches have similar performance characteristics. Default interface methods use virtual dispatch, while abstract methods provide direct inheritance. The JIT compiler optimizes both effectively in modern .NET.
Bottom line: Default interface methods excel at API evolution and optional behavior. Abstract methods dominate when you need shared state, dependency injection, and complex inheritance patterns.See other c-sharp posts