Why C# Chose a Different Path
Myth: C# doesn't support multiple inheritance, so you're stuck with single inheritance limitations. Reality: C# supports multiple inheritance of interfaces and behavior through default interface methods, giving you polymorphism without the diamond problem that plagues languages with multiple class inheritance.
Languages like C++ allow inheriting from multiple classes, which creates ambiguity when two base classes define the same method. C# sidesteps this by restricting class inheritance to a single base while allowing unlimited interface implementation. Interfaces define contracts without state, eliminating most ambiguity issues.
You'll learn how to implement multiple interfaces, use default interface methods to share behavior, combine composition with interfaces for flexible design, and understand when to choose interfaces versus abstract classes for your inheritance hierarchies.
Implementing Multiple Interfaces
A class can implement as many interfaces as needed, separated by commas. Each interface defines a contract that the class must fulfill by providing implementations for all members. This lets you compose types from multiple behavior contracts without ambiguity.
Here's a practical example where a class implements multiple interfaces to gain different capabilities. The class satisfies all interface contracts while inheriting implementation from a single base class.
public interface IConnectable
{
bool IsConnected { get; }
void Connect();
void Disconnect();
}
public interface IControllable
{
void TurnOn();
void TurnOff();
bool IsOn { get; }
}
public interface IMonitorable
{
string GetStatus();
Dictionary<string, object> GetMetrics();
}
public class SmartThermostat : IConnectable, IControllable, IMonitorable
{
private bool _isConnected;
private bool _isOn;
private double _currentTemp = 72.0;
// IConnectable implementation
public bool IsConnected => _isConnected;
public void Connect()
{
_isConnected = true;
Console.WriteLine("Thermostat connected to network");
}
public void Disconnect()
{
_isConnected = false;
Console.WriteLine("Thermostat disconnected");
}
// IControllable implementation
public bool IsOn => _isOn;
public void TurnOn()
{
_isOn = true;
Console.WriteLine("Thermostat powered on");
}
public void TurnOff()
{
_isOn = false;
Console.WriteLine("Thermostat powered off");
}
// IMonitorable implementation
public string GetStatus()
{
return $"Temperature: {_currentTemp}°F, " +
$"Connected: {_isConnected}, On: {_isOn}";
}
public Dictionary<string, object> GetMetrics()
{
return new Dictionary<string, object>
{
["CurrentTemperature"] = _currentTemp,
["IsConnected"] = _isConnected,
["IsOn"] = _isOn,
["Uptime"] = TimeSpan.FromHours(24)
};
}
}
The SmartThermostat implements three interfaces, each adding distinct capabilities. Consumers can work with the thermostat through any of these interfaces depending on their needs. This flexibility is the core benefit of multiple interface inheritance.
Default Interface Methods for Shared Behavior
C# 8 introduced default interface methods, allowing interfaces to provide behavior implementations. This enables you to add shared logic to interfaces while still allowing implementing classes to override when needed. It's not full multiple inheritance since interfaces can't have fields, but it provides behavior reuse.
public interface ILoggable
{
string ComponentName { get; }
// Default implementation provided
void Log(string message)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Console.WriteLine($"[{timestamp}] {ComponentName}: {message}");
}
void LogError(string error)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Console.WriteLine($"[{timestamp}] ERROR in {ComponentName}: {error}");
}
}
public interface IValidatable
{
bool IsValid();
// Default implementation
void ValidateOrThrow()
{
if (!IsValid())
{
throw new InvalidOperationException(
$"Validation failed for {GetType().Name}");
}
}
}
public class UserAccount : ILoggable, IValidatable
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// Required by ILoggable
public string ComponentName => "UserAccount";
// Required by IValidatable
public bool IsValid()
{
return !string.IsNullOrEmpty(Username) &&
!string.IsNullOrEmpty(Email) &&
Email.Contains("@");
}
public void CreateAccount()
{
// Use default interface methods
Log("Attempting to create account");
ValidateOrThrow();
Log($"Account created for {Username}");
}
// Can override default implementation if needed
void ILoggable.Log(string message)
{
// Custom logging implementation
Console.WriteLine($">>> {ComponentName} | {message}");
}
}
Default methods provide implementation that classes inherit automatically unless they override. This pattern lets you evolve interfaces by adding new methods with default implementations without breaking existing implementations. It's particularly useful for framework design and versioning scenarios.
Composition Over Inheritance
Sometimes the best approach to multiple inheritance is to avoid inheritance entirely and use composition. Instead of inheriting behavior from multiple sources, you compose objects by holding references to other objects that provide needed functionality. This gives you more flexibility and looser coupling.
// Instead of inheriting, we compose
public class Logger
{
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
public class Validator
{
public bool Validate(string input)
{
return !string.IsNullOrWhiteSpace(input);
}
public void ThrowIfInvalid(string input, string fieldName)
{
if (!Validate(input))
throw new ArgumentException($"{fieldName} is required");
}
}
public class NetworkClient
{
public void SendData(string data)
{
Console.WriteLine($"Sending: {data}");
}
}
// Composed class uses instances instead of inheritance
public class DataProcessor : IDisposable
{
private readonly Logger _logger;
private readonly Validator _validator;
private readonly NetworkClient _client;
public DataProcessor(Logger logger, Validator validator,
NetworkClient client)
{
_logger = logger;
_validator = validator;
_client = client;
}
public void ProcessAndSend(string data)
{
_logger.Log("Starting data processing");
_validator.ThrowIfInvalid(data, nameof(data));
_logger.Log("Validation passed");
var processed = data.ToUpper();
_client.SendData(processed);
_logger.Log("Processing complete");
}
public void Dispose()
{
_logger.Log("Disposing DataProcessor");
}
}
// Usage with dependency injection
var processor = new DataProcessor(
new Logger(),
new Validator(),
new NetworkClient()
);
processor.ProcessAndSend("hello world");
Composition allows each component to evolve independently. You can swap implementations at runtime, test components in isolation, and avoid the fragile base class problem. While it requires more explicit delegation, the trade-off is worth it for complex systems.
Choosing Between Interfaces and Abstract Classes
Interfaces define what a class can do without specifying how. Abstract classes provide partial implementation and shared state. Use interfaces when you need multiple type compatibility, and abstract classes when you need to share implementation details including fields and constructors.
// Interface: pure contract, no state
public interface IRepository<T>
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// Abstract class: shared implementation and state
public abstract class BaseRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
protected BaseRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public abstract Task<bool> ValidateAsync(T entity);
}
// Concrete class: inherits one base, implements multiple interfaces
public class ProductRepository : BaseRepository<Product>,
IRepository<Product>, ISearchable<Product>
{
public ProductRepository(DbContext context) : base(context) { }
public async Task AddAsync(Product entity)
{
await ValidateAsync(entity);
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Product entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
public override async Task<bool> ValidateAsync(Product entity)
{
return !string.IsNullOrEmpty(entity.Name) && entity.Price > 0;
}
// ISearchable implementation
public async Task<IEnumerable<Product>> SearchAsync(string term)
{
return await _dbSet
.Where(p => p.Name.Contains(term))
.ToListAsync();
}
}
This pattern combines the best of both approaches. BaseRepository provides shared state and common implementation. Interfaces like IRepository and ISearchable define contracts that allow polymorphic usage. The concrete class inherits implementation while satisfying multiple contracts.
Knowing the Limits
Multiple interface implementation isn't always the answer. When you find yourself implementing the same interface members identically across many classes, you're missing an abstraction. Extract that shared code into a base class or use composition with a shared component.
Avoid creating interfaces with dozens of members. Large interfaces violate the Interface Segregation Principle and make implementation burdensome. Split them into smaller, focused interfaces that clients can implement selectively. This also makes testing easier since mocks only need to implement relevant contracts.
Default interface methods have limitations too. They can't access instance state, so they're limited to working with interface members only. If you need shared state or constructor initialization, use an abstract base class instead. Save default methods for utility functions and extension-like behavior.
Try It Yourself
Build a console application that demonstrates multiple interface inheritance and default interface methods in action.
Steps
- Create project:
dotnet new console -n InterfaceDemo
- Navigate:
cd InterfaceDemo
- Replace Program.cs with code below
- Execute:
dotnet run
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Console.WriteLine("=== Multiple Interface Inheritance Demo ===\n");
var device = new SmartLight();
// Use as IControllable
IControllable controllable = device;
controllable.TurnOn();
// Use as IConnectable
IConnectable connectable = device;
connectable.Connect();
// Use as concrete type
device.SetBrightness(75);
Console.WriteLine($"\nFinal state: {device.GetStatus()}");
public interface IConnectable
{
void Connect();
bool IsConnected { get; }
}
public interface IControllable
{
void TurnOn();
void TurnOff();
bool IsOn { get; }
}
public class SmartLight : IConnectable, IControllable
{
private bool _isConnected;
private bool _isOn;
private int _brightness = 100;
public bool IsConnected => _isConnected;
public bool IsOn => _isOn;
public void Connect()
{
_isConnected = true;
Console.WriteLine("Smart light connected to network");
}
public void TurnOn()
{
_isOn = true;
Console.WriteLine("Smart light turned on");
}
public void TurnOff()
{
_isOn = false;
Console.WriteLine("Smart light turned off");
}
public void SetBrightness(int level)
{
_brightness = Math.Clamp(level, 0, 100);
Console.WriteLine($"Brightness set to {_brightness}%");
}
public string GetStatus()
{
return $"Connected: {_isConnected}, On: {_isOn}, " +
$"Brightness: {_brightness}%";
}
}
Output
=== Multiple Interface Inheritance Demo ===
Smart light turned on
Smart light connected to network
Brightness set to 75%
Final state: Connected: True, On: True, Brightness: 75%
The SmartLight class implements two interfaces, allowing it to be used polymorphically through either contract. This demonstrates how C# achieves multiple inheritance of behavior through interfaces.