Skip to content

C# - Interfaces

An interface defines a contract. It specifies a set of related functionalities (methods, properties, events, or indexers) that a class or struct must implement. Think of it as a blueprint for capabilities. An interface declares what a class can do, but not how it does it.

Interfaces are a cornerstone of object-oriented programming in C#. They enable polymorphism, facilitate dependency injection, and allow for the creation of loosely-coupled, testable, and maintainable systems.

Interfaces are declared using the interface keyword. By convention, interface names start with a capital I (e.g., ILogger).

// 1. Declare the interface (the contract)
public interface ILoggable
{
// A method that implementing classes must provide
void LogMessage(string message);
// A property that implementing classes must provide
string LogLevel { get; set; }
}
// 2. Implement the interface in a class
public class ConsoleLogger : ILoggable
{
public string LogLevel { get; set; } = "Info";
public void LogMessage(string message)
{
Console.WriteLine($"[{LogLevel.ToUpper()}]: {message}");
}
}
// 3. Use the implementation
ILoggable logger = new ConsoleLogger();
logger.LogLevel = "Warning";
logger.LogMessage("The system is running low on memory.");
/*
* Expected Output:
* [WARNING]: The system is running low on memory.
*/

Since C# 8.0, interfaces can provide a default implementation for their members. This is useful for adding new functionality to an existing interface without breaking all the classes that already implement it.

public interface ILoggable
{
void LogMessage(string message);
string LogLevel { get; set; }
// A default implementation. Classes are not required to implement this.
void LogError(Exception ex)
{
// Default behavior is to log the exception message as a critical error.
var originalLevel = LogLevel;
LogLevel = "Critical";
LogMessage($"An error occurred: {ex.Message}");
LogLevel = originalLevel;
}
}
// This class only implements the required members.
// It will inherit the default LogError behavior.
public class FileLogger : ILoggable
{
public string LogLevel { get; set; } = "Info";
private readonly string _filePath = "log.txt";
public void LogMessage(string message)
{
File.AppendAllText(_filePath, $"[{LogLevel.ToUpper()}]: {message}\n");
}
}
// Usage
ILoggable fileLogger = new FileLogger();
fileLogger.LogError(new InvalidOperationException("Disk full")); // Uses the default method

Both interfaces and abstract classes are used for abstraction, but they have different use cases:

FeatureInterfaceAbstract Class
Multiple InheritanceA class can implement multiple interfaces.A class can inherit from only one abstract class.
Fields and StateCannot contain instance fields (state).Can contain fields and state.
ConstructorsCannot have constructors.Can have constructors.
Primary Use CaseDefining a capability or role (e.g., IDisposable, IEquatable). Good for “can do” relationships.Defining a base type for a family of related objects. Good for “is a” relationships.

Guideline: Prefer interfaces for defining contracts that can be implemented by any class. Use an abstract class when you need to provide common, shared implementation details for a set of derived classes.

The .NET framework includes many powerful interfaces. Understanding them is key to writing idiomatic C#.

IEnumerable<T>: The Foundation of Iteration

Section titled “IEnumerable<T>: The Foundation of Iteration”

This interface allows an object to be iterated over, most commonly with a foreach loop. Implementing it is made incredibly simple with the yield return keyword.

using System.Collections;
using System.Collections.Generic;
// This class can now be used in a foreach loop.
public class MyNumberRange : IEnumerable<int>
{
private readonly int _start, _end;
public MyNumberRange(int start, int end)
{
_start = start;
_end = end;
}
// The compiler turns this into a state machine for you!
public IEnumerator<int> GetEnumerator()
{
for (int i = _start; i <= _end; i++)
{
yield return i;
}
}
// Non-generic version required for compatibility
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// Usage
var numbers = new MyNumberRange(1, 5);
foreach (var num in numbers)
{
Console.Write($"{num} "); // Output: 1 2 3 4 5
}

IComparable<T>: Providing a Natural Sort Order

Section titled “IComparable<T>: Providing a Natural Sort Order”

Implementing this interface allows objects of your class to be sorted by methods like List<T>.Sort() or used in sorted collections.

public class Product : IComparable<Product>
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
// Defines the default sort order (by Price).
public int CompareTo(Product? other)
{
if (other == null) return 1;
return this.Price.CompareTo(other.Price);
}
public override string ToString() => $"{Name} - ${Price}";
}
// Usage
var products = new List<Product>
{
new() { Name = "Laptop", Price = 1200m },
new() { Name = "Mouse", Price = 25m },
new() { Name = "Keyboard", Price = 75m }
};
products.Sort(); // Sorts the list in place
foreach (var product in products)
{
Console.WriteLine(product);
}
/*
* Expected Output:
* Mouse - $25
* Keyboard - $75
* Laptop - $1200
*/

Real-World Application: Dependency Injection

Section titled “Real-World Application: Dependency Injection”

Interfaces are essential for Dependency Injection (DI), a design pattern that promotes loose coupling. Instead of a class creating its own dependencies, they are “injected” from an external source. This makes code easier to test, as you can inject a mock or fake implementation of the interface during testing.

// The service depends on the ILogger interface, not a concrete class.
public class NotificationService
{
private readonly ILoggable _logger;
// The dependency is injected via the constructor.
public NotificationService(ILoggable logger)
{
_logger = logger;
}
public void SendNotification(string message)
{
_logger.LogMessage($"Sending notification: {message}");
// ... logic to send notification ...
}
}
// In production, you inject a real logger.
var consoleLogger = new ConsoleLogger();
var prodService = new NotificationService(consoleLogger);
// In a unit test, you could inject a fake logger.
// (Using a library like Moq or NSubstitute is common for this)
public class FakeLogger : ILoggable
{
public List<string> Messages { get; } = new();
public string LogLevel { get; set; } = "Test";
public void LogMessage(string message) => Messages.Add(message);
}
var fakeLogger = new FakeLogger();
var testService = new NotificationService(fakeLogger);
testService.SendNotification("Test");
// Assert.AreEqual(1, fakeLogger.Messages.Count); // Example of a test assertion