Skip to main content

3.5 - Interfaces

Interfaces are a fundamental feature in C# that define a contract for classes to implement. An interface contains declarations of methods, properties, events, and indexers, but does not provide implementations. Classes that implement an interface must provide implementations for all the members defined in the interface.

🔰 Beginner's Corner: What is an Interface?

Think of an interface like a contract or a promise that a class makes:

┌───────────────────────────────────────────────────┐
│ │
│ INTERFACE AS A CONTRACT │
│ │
│ ┌─────────────────┐ │
│ │ Interface │ │
│ │ IVehicle │ │
│ │ │ │
│ │ • Start() │ │
│ │ • Stop() │◄────────┐ │
│ │ • Accelerate() │ │ │
│ └─────────────────┘ │ │
│ │ │
│ │ "I promise to │
│ │ implement all │
│ │ these methods" │
│ │ │
│ ┌─────────────────┐ │ │
│ │ Class │ │ │
│ │ Car ├─────────┘ │
│ │ │ │
│ │ • Start() {...} │ │
│ │ • Stop() {...} │ │
│ │ • Accelerate() {...} │
│ │ • Park() {...} │ │
│ └─────────────────┘ │
│ │
└───────────────────────────────────────────────────┘

💡 Concept Breakdown: Why Use Interfaces?

Interfaces solve several important problems in programming:

  1. Define a contract - They specify exactly what methods and properties a class must implement

  2. Enable polymorphism - They allow different classes to be treated the same way:

    // These could be completely different classes
    IVehicle car = new Car();
    IVehicle boat = new Boat();
    IVehicle plane = new Plane();

    // But we can treat them the same way
    vehicle.Start();
    vehicle.Accelerate();
  3. Support multiple inheritance - While a class can only inherit from one base class, it can implement many interfaces:

    // A class can implement multiple interfaces
    public class SmartPhone : IPhone, ICamera, IWebBrowser, IGPSDevice
    {
    // Must implement all members from all interfaces
    }

🌟 Real-World Analogy: Job Descriptions

Think of interfaces like job descriptions:

  • A job description lists responsibilities but doesn't say how to do them
  • Different people (classes) can fill the same role if they can perform all the required duties
  • One person can fulfill multiple job descriptions (interfaces) at the same time
  • The employer doesn't need to know exactly how you do your job, just that you can do what's required

3.5.1 - Interface Definition

An interface is defined using the interface keyword, followed by the interface name (conventionally starting with "I") and a pair of curly braces that enclose the interface members.

// Basic interface definition
public interface IShape
{
// Property declaration
double Area { get; }

// Method declaration
double CalculatePerimeter();

// Method with parameters
void Scale(double factor);

// Default implementation (C# 8+)
void Draw()
{
Console.WriteLine("Drawing a shape");
}
}

// Interface with events
public interface INotifyPropertyChanged
{
// Event declaration
event PropertyChangedEventHandler PropertyChanged;

// Method declaration
void RaisePropertyChanged(string propertyName);
}

// Interface with indexers
public interface IDataStore
{
// Indexer declaration
object this[string key] { get; set; }

// Method declarations
bool ContainsKey(string key);
void Remove(string key);
}

3.5.2 - Implementing Interfaces

A class implements an interface by including the interface name in its base list and providing implementations for all the members defined in the interface.

// Implementing a single interface
public class Circle : IShape
{
public double Radius { get; set; }

public Circle(double radius)
{
Radius = radius;
}

// Implementing the Area property
public double Area => Math.PI * Radius * Radius;

// Implementing the CalculatePerimeter method
public double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}

// Implementing the Scale method
public void Scale(double factor)
{
Radius *= factor;
}

// Overriding the default implementation (C# 8+)
public void Draw()
{
Console.WriteLine($"Drawing a circle with radius {Radius}");
}
}

// Implementing multiple interfaces
public class Rectangle : IShape, ICloneable
{
public double Width { get; set; }
public double Height { get; set; }

public Rectangle(double width, double height)
{
Width = width;
Height = height;
}

// Implementing IShape members
public double Area => Width * Height;

public double CalculatePerimeter()
{
return 2 * (Width + Height);
}

public void Scale(double factor)
{
Width *= factor;
Height *= factor;
}

// Implementing ICloneable members
public object Clone()
{
return new Rectangle(Width, Height);
}
}

// Implementing an interface with events
public class Person : INotifyPropertyChanged
{
private string name;

public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
RaisePropertyChanged(nameof(Name));
}
}
}

// Implementing the event
public event PropertyChangedEventHandler PropertyChanged;

// Implementing the method
public void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Using Interface Implementations

// Creating objects that implement interfaces
IShape circle = new Circle(5);
IShape rectangle = new Rectangle(4, 6);

// Using interface members
Console.WriteLine($"Circle Area: {circle.Area}");
Console.WriteLine($"Circle Perimeter: {circle.CalculatePerimeter()}");
circle.Scale(2);
Console.WriteLine($"Circle Area after scaling: {circle.Area}");

Console.WriteLine($"Rectangle Area: {rectangle.Area}");
Console.WriteLine($"Rectangle Perimeter: {rectangle.CalculatePerimeter()}");
rectangle.Scale(1.5);
Console.WriteLine($"Rectangle Area after scaling: {rectangle.Area}");

// Using the default implementation (C# 8+)
circle.Draw();
rectangle.Draw();

// Using multiple interfaces
Rectangle rect = new Rectangle(4, 6);
IShape shapeInterface = rect;
ICloneable cloneableInterface = rect;

Console.WriteLine($"Rectangle Area: {shapeInterface.Area}");
Rectangle clonedRect = (Rectangle)cloneableInterface.Clone();
Console.WriteLine($"Cloned Rectangle Area: {((IShape)clonedRect).Area}");

// Using events
Person person = new Person();
person.PropertyChanged += (sender, e) =>
{
Console.WriteLine($"Property {e.PropertyName} changed");
};
person.Name = "John"; // Triggers the event

3.5.3 - Interface Inheritance

Interfaces can inherit from one or more other interfaces, creating a hierarchy of interfaces. A class that implements a derived interface must implement all members from both the derived interface and its base interfaces.

// Base interfaces
public interface IDrawable
{
void Draw();
}

public interface IScalable
{
void Scale(double factor);
}

// Derived interface inheriting from multiple interfaces
public interface IGraphic : IDrawable, IScalable
{
void Move(double x, double y);
}

// Implementing a derived interface
public class Graphic : IGraphic
{
public double X { get; private set; }
public double Y { get; private set; }
public double Size { get; private set; }

public Graphic(double x, double y, double size)
{
X = x;
Y = y;
Size = size;
}

// Implementing IDrawable
public void Draw()
{
Console.WriteLine($"Drawing a graphic at ({X}, {Y}) with size {Size}");
}

// Implementing IScalable
public void Scale(double factor)
{
Size *= factor;
}

// Implementing IGraphic
public void Move(double x, double y)
{
X += x;
Y += y;
}
}

Using Interface Inheritance

// Creating an object that implements a derived interface
Graphic graphic = new Graphic(10, 20, 5);

// Using the object through different interface references
IDrawable drawable = graphic;
IScalable scalable = graphic;
IGraphic graphicInterface = graphic;

// Calling methods through different interfaces
drawable.Draw();
scalable.Scale(2);
graphicInterface.Move(5, 10);

// The object's state is affected regardless of which interface is used
drawable.Draw(); // Shows the updated position and size

3.5.4 - Explicit Interface Implementation

When a class implements multiple interfaces that have members with the same name, or when a class wants to implement an interface member without making it part of its public API, it can use explicit interface implementation.

// Interfaces with conflicting member names
public interface IA
{
void Method();
int Property { get; set; }
}

public interface IB
{
void Method();
string Property { get; set; }
}

// Class implementing both interfaces explicitly
public class ConflictingImplementation : IA, IB
{
// Explicit implementation of IA.Method
void IA.Method()
{
Console.WriteLine("IA.Method implementation");
}

// Explicit implementation of IB.Method
void IB.Method()
{
Console.WriteLine("IB.Method implementation");
}

// Explicit implementation of IA.Property
int IA.Property { get; set; }

// Explicit implementation of IB.Property
string IB.Property { get; set; }

// Regular method that can call the interface implementations
public void CallMethods()
{
((IA)this).Method();
((IB)this).Method();
}
}

// Class implementing an interface explicitly for encapsulation
public class EncapsulatedImplementation : IDisposable
{
private bool disposed = false;

// Public method
public void DoWork()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(EncapsulatedImplementation));
}

Console.WriteLine("Doing work");
}

// Explicit interface implementation
void IDisposable.Dispose()
{
if (!disposed)
{
// Clean up resources
Console.WriteLine("Disposing resources");
disposed = true;
}
}
}

Using Explicit Interface Implementation

// Creating an object with explicit interface implementations
ConflictingImplementation obj = new ConflictingImplementation();

// Cannot call the interface methods directly on the object
// obj.Method(); // Error: 'ConflictingImplementation' does not contain a definition for 'Method'

// Must cast to the specific interface
IA a = obj;
IB b = obj;

a.Method(); // Calls IA.Method
b.Method(); // Calls IB.Method

a.Property = 42;
b.Property = "Hello";

Console.WriteLine($"IA.Property: {a.Property}");
Console.WriteLine($"IB.Property: {b.Property}");

// Can call a method that internally calls the interface implementations
obj.CallMethods();

// Using explicit implementation for encapsulation
EncapsulatedImplementation encapsulated = new EncapsulatedImplementation();
encapsulated.DoWork(); // Works fine

// Must cast to the interface to call Dispose
IDisposable disposable = encapsulated;
disposable.Dispose();

// After disposing, DoWork throws an exception
try
{
encapsulated.DoWork();
}
catch (ObjectDisposedException ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}

3.5.5 - Default Interface Methods (C# 8+)

Starting with C# 8.0, interfaces can include default implementations for methods, properties, indexers, and events. This allows interfaces to evolve over time without breaking existing implementations.

// Interface with default methods
public interface ILogger
{
// Method without a default implementation
void Log(string message);

// Method with a default implementation
void LogError(string message)
{
Log($"ERROR: {message}");
}

// Method with a default implementation that calls another default method
void LogWarning(string message)
{
Log($"WARNING: {message}");
}

// Property with a default implementation
LogLevel Level { get; set; } = LogLevel.Info;

// Method that uses the property
bool ShouldLog(LogLevel level)
{
return level >= Level;
}
}

public enum LogLevel
{
Debug,
Info,
Warning,
Error
}

// Minimal implementation of the interface
public class ConsoleLogger : ILogger
{
public LogLevel Level { get; set; } = LogLevel.Info;

public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
}

// Implementation that overrides a default method
public class DetailedConsoleLogger : ILogger
{
public LogLevel Level { get; set; } = LogLevel.Debug;

public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}

public void LogError(string message)
{
Console.WriteLine($"[{DateTime.Now}] ERROR: {message}");
Console.WriteLine($"Stack Trace: {Environment.StackTrace}");
}
}

Using Default Interface Methods

// Creating objects that implement the interface
ILogger consoleLogger = new ConsoleLogger();
ILogger detailedLogger = new DetailedConsoleLogger();

// Using the interface methods
consoleLogger.Log("Regular message");
consoleLogger.LogError("Something went wrong"); // Uses the default implementation
consoleLogger.LogWarning("This is a warning"); // Uses the default implementation

detailedLogger.Log("Regular message");
detailedLogger.LogError("Something went wrong"); // Uses the overridden implementation
detailedLogger.LogWarning("This is a warning"); // Uses the default implementation

// Using the property and method
consoleLogger.Level = LogLevel.Warning;
Console.WriteLine($"Should log Info: {consoleLogger.ShouldLog(LogLevel.Info)}"); // False
Console.WriteLine($"Should log Warning: {consoleLogger.ShouldLog(LogLevel.Warning)}"); // True
Console.WriteLine($"Should log Error: {consoleLogger.ShouldLog(LogLevel.Error)}"); // True

3.5.6 - Ref Struct Interfaces (C# 13+)

Starting with C# 13, ref struct types can implement interfaces, which was not possible in earlier versions. This feature enables better integration of high-performance, stack-allocated types with the rest of the C# type system.

3.5.6.1 - Understanding Ref Struct Types

A ref struct is a value type that must be allocated on the stack rather than the heap. This restriction enables performance optimizations but also imposes limitations on how these types can be used.

// Basic ref struct definition
public ref struct Point
{
public int X;
public int Y;

public Point(int x, int y)
{
X = x;
Y = y;
}

public double Distance => Math.Sqrt(X * X + Y * Y);
}

3.5.6.2 - Implementing Interfaces with Ref Structs

C# 13 allows ref struct types to implement interfaces, but with certain restrictions to maintain ref safety:

// Interface definition
public interface IGeometricShape
{
double Area { get; }
double Perimeter { get; }
void Scale(double factor);
}

// Ref struct implementing an interface
public ref struct Rectangle : IGeometricShape
{
public int Width;
public int Height;

public Rectangle(int width, int height)
{
Width = width;
Height = height;
}

// Implementing interface members
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);

public void Scale(double factor)
{
Width = (int)(Width * factor);
Height = (int)(Height * factor);
}
}

3.5.6.3 - Restrictions and Safety Rules

To maintain ref safety, there are important restrictions when using interfaces with ref struct types:

  1. A ref struct cannot be boxed, so it cannot be converted to an interface type.
  2. Interface methods can only be accessed through a type parameter that allows ref struct types.
  3. A ref struct must implement all interface methods, even those with default implementations.
// Using ref struct interfaces with generics
public static class GeometryOperations<T> where T : IGeometricShape, allows ref struct
{
public static double CalculateAreaToPerimeterRatio(T shape)
{
return shape.Area / shape.Perimeter;
}

public static void DoubleSize(T shape)
{
shape.Scale(2.0);
}
}

// Usage
Rectangle rect = new Rectangle(5, 10);
double ratio = GeometryOperations<Rectangle>.CalculateAreaToPerimeterRatio(rect);
Console.WriteLine($"Area to perimeter ratio: {ratio}");

GeometryOperations<Rectangle>.DoubleSize(rect);
Console.WriteLine($"After scaling - Width: {rect.Width}, Height: {rect.Height}");

// This would cause a compilation error:
// IGeometricShape shape = rect; // Error: Cannot convert ref struct to interface type

3.5.6.4 - Practical Applications

The ability for ref struct types to implement interfaces is particularly useful for high-performance scenarios, such as:

  1. Low-level memory manipulation: Working with Span<T> and ReadOnlySpan<T> in a more object-oriented way.
  2. Graphics and game development: Using stack-allocated geometric primitives that implement common interfaces.
  3. Signal processing: Creating efficient data processing pipelines with stack-allocated buffers.
// Interface for buffer operations
public interface IBuffer<T>
{
int Length { get; }
ref T GetReference(int index);
void Fill(T value);
}

// Ref struct implementing the buffer interface
public ref struct StackBuffer<T> : IBuffer<T> where T : struct
{
private Span<T> _data;

public StackBuffer(Span<T> data)
{
_data = data;
}

public int Length => _data.Length;

public ref T GetReference(int index) => ref _data[index];

public void Fill(T value)
{
_data.Fill(value);
}
}

// Usage with stack-allocated array
Span<int> span = stackalloc int[100];
StackBuffer<int> buffer = new StackBuffer<int>(span);
buffer.Fill(42);

// Process the buffer using generic algorithm
ProcessBuffer(buffer);

// Generic method that works with any IBuffer implementation
static void ProcessBuffer<T, TBuffer>(TBuffer buffer)
where T : struct
where TBuffer : IBuffer<T>, allows ref struct
{
for (int i = 0; i < buffer.Length; i++)
{
ref T item = ref buffer.GetReference(i);
// Process the item...
}
}

The ability for ref struct types to implement interfaces bridges the gap between high-performance, low-level code and the object-oriented design principles that make C# code maintainable and extensible.

3.5.7 - Interface Properties and Events

Interfaces can declare properties and events, which must be implemented by classes that implement the interface.

// Interface with properties and events
public interface INotifiable
{
// Read-only property
string Name { get; }

// Read-write property
bool IsActive { get; set; }

// Event
event EventHandler<NotificationEventArgs> Notified;

// Method that raises the event
void Notify(string message);
}

// Event args class
public class NotificationEventArgs : EventArgs
{
public string Message { get; }
public DateTime Timestamp { get; }

public NotificationEventArgs(string message)
{
Message = message;
Timestamp = DateTime.Now;
}
}

// Implementing the interface
public class NotificationService : INotifiable
{
private bool isActive;

public NotificationService(string name)
{
Name = name;
IsActive = true;
}

// Implementing the properties
public string Name { get; }

public bool IsActive
{
get => isActive;
set
{
isActive = value;
Console.WriteLine($"Notification service '{Name}' is now {(isActive ? "active" : "inactive")}");
}
}

// Implementing the event
public event EventHandler<NotificationEventArgs> Notified;

// Implementing the method
public void Notify(string message)
{
if (!IsActive)
{
Console.WriteLine($"Cannot send notification: service '{Name}' is inactive");
return;
}

Console.WriteLine($"Sending notification from '{Name}': {message}");
OnNotified(new NotificationEventArgs(message));
}

// Helper method to raise the event
protected virtual void OnNotified(NotificationEventArgs e)
{
Notified?.Invoke(this, e);
}
}

Using Interface Properties and Events

// Creating an object that implements the interface
INotifiable service = new NotificationService("Email Service");

// Subscribing to the event
service.Notified += (sender, e) =>
{
Console.WriteLine($"Notification received at {e.Timestamp}: {e.Message}");
};

// Using the properties
Console.WriteLine($"Service Name: {service.Name}");
Console.WriteLine($"Is Active: {service.IsActive}");

// Calling the method
service.Notify("Hello, World!");

// Changing the property
service.IsActive = false;

// Calling the method again
service.Notify("This won't be sent");

3.5.7 - Interface Best Practices

When designing and using interfaces in C#, consider the following best practices:

  1. Keep Interfaces Focused: Interfaces should have a single, well-defined purpose. Follow the Interface Segregation Principle (ISP) from SOLID principles, which states that clients should not be forced to depend on methods they do not use.

  2. Name Interfaces Appropriately: Interface names should typically start with "I" and describe the behavior they represent (e.g., IComparable, IDisposable, IEnumerable).

  3. Design for Extension: When designing interfaces, consider how they might need to evolve over time. With C# 8.0 and later, you can add default implementations to interfaces, but it's still better to design them well from the start.

  4. Use Interfaces for Abstraction: Interfaces are a powerful tool for abstraction and dependency inversion. Use them to decouple components and make your code more testable and maintainable.

  5. Consider Interface Inheritance Carefully: While interfaces can inherit from other interfaces, complex inheritance hierarchies can be confusing. Use interface inheritance judiciously.

  6. Document Interface Contracts: Clearly document the expected behavior of interface members, including any assumptions or requirements.

  7. Use Explicit Implementation When Appropriate: Use explicit interface implementation to resolve naming conflicts or to hide interface members from the public API of a class.

  8. Avoid Large Interfaces: Large interfaces with many members can be difficult to implement and maintain. Consider breaking them down into smaller, more focused interfaces.

  9. Prefer Composition Over Interface Inheritance: When possible, use composition (implementing multiple interfaces) rather than interface inheritance to combine behaviors.

  10. Be Consistent with Default Implementations: If you use default implementations (C# 8+), be consistent in how you use them across your codebase.

Example of Good Interface Design

// Small, focused interfaces
public interface IReadable
{
string Read();
bool CanRead { get; }
}

public interface IWritable
{
void Write(string content);
bool CanWrite { get; }
}

public interface ICloseable
{
void Close();
bool IsClosed { get; }
}

// Combining interfaces through composition
public interface IFile : IReadable, IWritable, ICloseable
{
string Path { get; }
long Size { get; }
}

// Implementation
public class TextFile : IFile
{
private bool isClosed;
private string content;

public TextFile(string path, string initialContent = "")
{
Path = path;
content = initialContent;
isClosed = false;
}

// IReadable implementation
public string Read()
{
if (IsClosed)
throw new InvalidOperationException("Cannot read from a closed file");

return content;
}

public bool CanRead => !IsClosed;

// IWritable implementation
public void Write(string newContent)
{
if (IsClosed)
throw new InvalidOperationException("Cannot write to a closed file");

content = newContent;
}

public bool CanWrite => !IsClosed;

// ICloseable implementation
public void Close()
{
isClosed = true;
}

public bool IsClosed => isClosed;

// IFile implementation
public string Path { get; }

public long Size => content.Length;
}

// Service that depends on interfaces
public class FileProcessor
{
public string ProcessReadable(IReadable readable)
{
if (!readable.CanRead)
return "Cannot read from the source";

string content = readable.Read();
return $"Processed content (length: {content.Length})";
}

public void ProcessWritable(IWritable writable, string content)
{
if (!writable.CanWrite)
throw new InvalidOperationException("Cannot write to the destination");

writable.Write(content);
}

public void CloseIfPossible(object obj)
{
if (obj is ICloseable closeable && !closeable.IsClosed)
{
closeable.Close();
}
}
}

Using Well-Designed Interfaces

// Creating an object that implements multiple interfaces
TextFile file = new TextFile("/path/to/file.txt", "Initial content");

// Using the object through different interface references
IReadable readable = file;
IWritable writable = file;
ICloseable closeable = file;
IFile fileInterface = file;

// Using a service that depends on interfaces
FileProcessor processor = new FileProcessor();

// Processing the file as a readable
string result = processor.ProcessReadable(readable);
Console.WriteLine(result);

// Processing the file as a writable
processor.ProcessWritable(writable, "New content");
Console.WriteLine($"File content: {readable.Read()}");

// Closing the file
processor.CloseIfPossible(file);
Console.WriteLine($"File is closed: {closeable.IsClosed}");

// Trying to read or write after closing
try
{
readable.Read();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}

try
{
writable.Write("More content");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}

In the next section, we'll explore advanced OOP features in C#, including indexers, extension methods, partial classes, and more.