Site

Interfaces — Contracts and Polymorphism

Tutorial 7.0  •  C# / Learn

7.0 What This Teaches

This tutorial covers how C# uses interfaces to define contracts:

7.1 Interface Syntax

An interface declares a contract - a set of members any implementing type must provide. No implementation lives in the interface (with the exception of default methods added in C# 8):
interface IShape
{
    double Area();
    double Perimeter();
    string Describe() => $"area={Area():F2}";  // default method (C# 8+)
}
By convention, interface names start with I. Members declared in an interface are implicitly public and abstract.

7.2 Implementing Interfaces

List the interface after the class name (or after the base class if there is one). The class must implement every member declared in the interface:
class Circle : IShape
{
    public double Radius { get; }
    public Circle(double r) { Radius = r; }

    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter() => 2 * Math.PI * Radius;
}

class Square : IShape
{
    public double Side { get; }
    public Square(double s) { Side = s; }

    public double Area() => Side * Side;
    public double Perimeter() => 4 * Side;
}

IShape c = new Circle(3);
IShape s = new Square(4);
Console.WriteLine(c.Describe());  // area=28.27
Console.WriteLine(s.Describe());  // area=16.00

7.3 Multiple Interfaces

A class can implement many interfaces. This is how C# achieves multiple inheritance of behavior - C# allows only one base class but any number of interfaces:
interface IDrawable
{
    void Draw(string canvas);
}

interface IResizable
{
    void Resize(double factor);
}

class Widget : IDrawable, IResizable
{
    public double Width { get; private set; }
    public double Height { get; private set; }

    public Widget(double w, double h) { Width = w; Height = h; }

    public void Draw(string canvas) =>
        Console.WriteLine($"Drawing {Width}x{Height} on {canvas}");

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

7.4 Interface References

An interface reference can hold any object that implements it. This is the foundation of polymorphism - code that depends on an interface works with any implementing type, including ones that didn't exist when the code was written:
static void PrintArea(IShape shape) =>
    Console.WriteLine($"{shape.GetType().Name}: area={shape.Area():F2}");

List<IShape> shapes = new() { new Circle(3), new Square(4), new Circle(1) };
foreach (var s in shapes)
    PrintArea(s);
The is operator tests whether an object implements an interface at runtime: if (obj is IDisposable d) d.Dispose();

7.5 Common Built-in Interfaces

Implementing standard interfaces integrates your type with the .NET ecosystem:
class Temperature : IComparable<Temperature>
{
    public double Celsius { get; }
    public Temperature(double c) { Celsius = c; }

    public int CompareTo(Temperature? other)
    {
        if (other is null) return 1;
        return Celsius.CompareTo(other.Celsius);
    }

    public override string ToString() => $"{Celsius}°C";
}

var temps = new[] { new Temperature(100), new Temperature(0), new Temperature(37) };
Array.Sort(temps);
foreach (var t in temps) Console.Write($"{t} ");
Expected output:
0°C 37°C 100°C
Other frequently used interfaces: IEnumerable<T> (enables foreach), IDisposable (enables using statements), IEquatable<T> (typed equality).

7.6 Explicit Interface Implementation

When two interfaces declare the same member name, use explicit implementation to satisfy both without ambiguity:
interface IA { void Do(); }
interface IB { void Do(); }

class Dual : IA, IB
{
    void IA.Do() => Console.WriteLine("IA.Do");
    void IB.Do() => Console.WriteLine("IB.Do");
}

Dual d = new Dual();
((IA)d).Do();  // IA.Do
((IB)d).Do();  // IB.Do
Explicitly implemented members are only accessible through the interface type, not through the class type. This is also useful to hide implementation details.

7.7 Example - All Together

// Interfaces - pluggable serialization via interface.

using System.Text.Json;

ISerializer jsonSerializer = new JsonSerializer();
ISerializer csvSerializer = new CsvSerializer();

var data = new[] { ("Alice", 30), ("Bob", 25) };

Console.WriteLine(jsonSerializer.Serialize(data));
Console.WriteLine(csvSerializer.Serialize(data));

interface ISerializer
{
    string Serialize(IEnumerable<(string Name, int Age)> rows);
}

class JsonSerializer : ISerializer
{
    public string Serialize(IEnumerable<(string Name, int Age)> rows)
    {
        var items = rows.Select(r => new { r.Name, r.Age });
        return System.Text.Json.JsonSerializer.Serialize(items);
    }
}

class CsvSerializer : ISerializer
{
    public string Serialize(IEnumerable<(string Name, int Age)> rows)
    {
        var lines = rows.Select(r => $"{r.Name},{r.Age}");
        return "Name,Age\n" + string.Join("\n", lines);
    }
}

7.8 Exercise

Exercise
  • Define an ILogger interface with void Log(string message) and string Name { get; }.
  • Implement it in ConsoleLogger (prints to stdout) and ListLogger (stores messages in a List<string> and exposes them).
  • Write a method that accepts ILogger and logs three messages through it. Call it with both implementations.

7.9 Common Mistakes

Implementing the wrong method signature

If your implementation method has a slightly different signature (wrong return type, extra parameter, wrong name), the compiler reports the interface is not fully implemented. Compare the interface declaration and your method signature character by character.

Casting to interface when is/as is safer

IDisposable d = (IDisposable)obj;  // throws InvalidCastException if obj doesn't implement it
if (obj is IDisposable id) id.Dispose();  // safe pattern matching - no exception

Putting too much in an interface

Interfaces define contracts, not convenience. A bloated interface forces every implementor to provide methods it may not need. Prefer small, focused interfaces (Interface Segregation Principle).

7.10 Key Terms

TermMeaning
interfaceContract declaring members a class must implement; no fields, no state
implementProvide a concrete body for each member declared by an interface
IComparable<T>Interface enabling sorting; requires CompareTo method
IEnumerable<T>Interface enabling foreach; requires GetEnumerator
IDisposableInterface enabling using statements; requires Dispose method
default methodInterface member with a body (C# 8+); used for optional or shared behavior
explicit implementationQualifying a member with the interface name to resolve ambiguity
is / asSafe runtime interface checks; is tests, as casts (returns null on failure)