Interfaces

An interface is a contract: a list of members a type promises to provide, with no say in how. Any class or struct can implement any number of interfaces, so unrelated types — a file, a database, a test fake — can be used interchangeably wherever that contract is expected. Interfaces are everywhere in .NET, from IEnumerable<T> to IDisposable.

Declaring and implementing an interface

By convention, interface names start with I. A class implements an interface by listing it after a colon and providing every member:

C#
IShape[] shapes = [new Circle(1), new Square(3)];
foreach (IShape s in shapes)
{
    Console.WriteLine($"{s.Name}: {s.Area():F2}");
}

interface IShape
{
    string Name { get; }
    double Area();
}

class Circle(double r) : IShape
{
    public string Name => "circle";
    public double Area() => Math.PI * r * r;
}

class Square(double side) : IShape
{
    public string Name => "square";
    public double Area() => side * side;
}
Output
circle: 3.14
square: 9.00

Interface members are public by default, and the implementing members must be public. Unlike overriding, no override keyword is needed.

Several interfaces at once

A class can have only one base class, but it can implement as many interfaces as it likes. This lets you describe separate capabilities:

C#
var doc = new Document("Q3 plan");
doc.Save();
doc.Print();

List<IPrintable> printQueue = [doc, new Photo("beach.jpg")];
foreach (IPrintable p in printQueue)
    p.Print();

interface ISaveable { void Save(); }
interface IPrintable { void Print(); }

class Document(string title) : ISaveable, IPrintable
{
    public void Save() => Console.WriteLine($"Saving '{title}'");
    public void Print() => Console.WriteLine($"Printing '{title}'");
}

class Photo(string file) : IPrintable
{
    public void Print() => Console.WriteLine($"Printing photo {file}");
}
Output
Saving 'Q3 plan'
Printing 'Q3 plan'
Printing 'Q3 plan'
Printing photo beach.jpg

Document and Photo share no base class, yet both can go in a list of IPrintable.

Programming to an interface

When a class depends on an interface rather than a concrete class, you can swap implementations without changing it. Here a checkout works with any payment method — including a fake one for testing:

C#
var checkout = new Checkout(new CardPayment("4111111111111111"));
checkout.PlaceOrder(59.90m);

var testCheckout = new Checkout(new FakePayment());
testCheckout.PlaceOrder(10m);

interface IPaymentMethod
{
    bool Charge(decimal amount);
}

class CardPayment(string cardNumber) : IPaymentMethod
{
    public bool Charge(decimal amount)
    {
        Console.WriteLine($"Charging {amount:C} to card ending {cardNumber[^4..]}");
        return true;
    }
}

class FakePayment : IPaymentMethod
{
    public bool Charge(decimal amount)
    {
        Console.WriteLine($"(test) pretend to charge {amount:C}");
        return true;
    }
}

class Checkout(IPaymentMethod payment)
{
    public void PlaceOrder(decimal total)
    {
        if (payment.Charge(total))
            Console.WriteLine("Order confirmed.");
    }
}
Output
Charging $59.90 to card ending 1111
Order confirmed.
(test) pretend to charge $10.00
Order confirmed.

Passing dependencies in through the constructor like this is called dependency injection. ASP.NET Core and most .NET applications are built around it.

Built-in interfaces: IComparable<T>

Implementing a library interface plugs your type into existing features. IComparable<T> defines a natural order, which Sort, Max and sorted collections use:

C#
List<Version> versions = [new(2, 1, 0), new(1, 9, 3), new(2, 0, 11), new(1, 10, 0)];
versions.Sort();
Console.WriteLine(string.Join("  ", versions));
Console.WriteLine($"Latest: {versions.Max()}");

class Version(int major, int minor, int patch) : IComparable<Version>
{
    public int Major { get; } = major;
    public int Minor { get; } = minor;
    public int Patch { get; } = patch;

    public int CompareTo(Version? other)
    {
        if (other is null) return 1;
        int c = Major.CompareTo(other.Major);
        if (c != 0) return c;
        c = Minor.CompareTo(other.Minor);
        return c != 0 ? c : Patch.CompareTo(other.Patch);
    }

    public override string ToString() => $"{Major}.{Minor}.{Patch}";
}
Output
1.9.3  1.10.0  2.0.11  2.1.0
Latest: 2.1.0

Other common ones: IEnumerable<T> (can be looped over with foreach — lesson 51), IDisposable (releases resources — lesson 53), IEquatable<T> (equality — lesson 42).

Interfaces as types

You can't create an instance of an interface, but you can use it as the type of variables, parameters, return values and collection elements. The is operator checks whether an object implements one:

C#
object[] things = [new Robot(), "text", new Robot()];
int charged = 0;
foreach (object o in things)
{
    if (o is IRechargeable r)
    {
        r.Recharge();
        charged++;
    }
}
Console.WriteLine($"Recharged {charged} devices");

interface IRechargeable { void Recharge(); }
class Robot : IRechargeable
{
    public void Recharge() => Console.WriteLine("Robot charging...");
}
Output
Robot charging...
Robot charging...
Recharged 2 devices

Default interface methods

An interface can provide a default body for a member. Implementing classes get it for free but can supply their own. This lets library authors add members to an interface without breaking existing implementations:

C#
ILogger console = new ConsoleLogger();
console.Log("Started");
console.LogError("Disk full");

ILogger custom = new LoudLogger();
custom.LogError("Disk full");

interface ILogger
{
    void Log(string message);
    void LogError(string message) => Log($"ERROR: {message}");
}

class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}

class LoudLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
    public void LogError(string message) => Console.WriteLine($"!!! {message.ToUpper()} !!!");
}
Output
Started
ERROR: Disk full
!!! DISK FULL !!!

Default methods are only reachable through the interface type — note that the variables above are declared as ILogger.

Explicit implementation

If two interfaces have a member with the same name, or you want to keep a member out of the class's normal public surface, implement it explicitly by prefixing it with the interface name. It's then callable only through the interface:

C#
var p = new Person();
Console.WriteLine(((IFrench)p).Greet());
Console.WriteLine(((ISpanish)p).Greet());

interface IFrench { string Greet(); }
interface ISpanish { string Greet(); }

class Person : IFrench, ISpanish
{
    string IFrench.Greet() => "Bonjour";
    string ISpanish.Greet() => "Hola";
}
Output
Bonjour
Hola

Static abstract members

Interfaces can also require static members, which lets generic code call them on a type parameter. This is how .NET's "generic math" works: INumber<T> requires operators like + and a static Zero. You'll see it in lesson 45:

C#
Console.WriteLine(Total([1, 2, 3]));
Console.WriteLine(Total([1.5, 2.25]));
Console.WriteLine(Total([10m, 0.5m]));

static T Total<T>(T[] values) where T : System.Numerics.INumber<T>
{
    T sum = T.Zero;
    foreach (T v in values) sum += v;
    return sum;
}
Output
6
3.75
10.5

Try it yourself

Implement IDescribable on two unrelated classes and print them together.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Porto, population 232,000
"Blue in Green" (5:37)