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:
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; }
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:
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}"); }
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:
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."); } }
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:
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}"; }
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:
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..."); }
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:
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()} !!!"); }
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:
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"; }
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:
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; }
6 3.75 10.5
Try it yourself
Implement IDescribable on two unrelated classes and print them together.
Porto, population 232,000 "Blue in Green" (5:37)