C# - Interfaces
C# - Interfaces
Section titled “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.
Declaring and Implementing an Interface
Section titled “Declaring and Implementing an Interface”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 classpublic class ConsoleLogger : ILoggable{ public string LogLevel { get; set; } = "Info";
public void LogMessage(string message) { Console.WriteLine($"[{LogLevel.ToUpper()}]: {message}"); }}
// 3. Use the implementationILoggable 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.*/Modern Feature: Default Interface Methods
Section titled “Modern Feature: Default Interface Methods”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"); }}
// UsageILoggable fileLogger = new FileLogger();fileLogger.LogError(new InvalidOperationException("Disk full")); // Uses the default methodInterfaces vs. Abstract Classes
Section titled “Interfaces vs. Abstract Classes”Both interfaces and abstract classes are used for abstraction, but they have different use cases:
| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple Inheritance | A class can implement multiple interfaces. | A class can inherit from only one abstract class. |
| Fields and State | Cannot contain instance fields (state). | Can contain fields and state. |
| Constructors | Cannot have constructors. | Can have constructors. |
| Primary Use Case | Defining 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.
Common .NET Interfaces
Section titled “Common .NET Interfaces”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();}
// Usagevar 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}";}
// Usagevar 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