Interfaces
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
An interface is a contract: a set of members a type promises to provide. Unlike a base class it carries no state, and a type may implement any number of them. Since C# 8 an interface may also supply default implementations, and since C# 11 it may declare static abstract members — which is what made generic math possible.
Declaring and Implementing
public interface IShape
{
double Area { get; } // property
double Perimeter { get; }
string Describe(); // method
event EventHandler? Changed; // event
}
public sealed class Rectangle : IShape
{
public Rectangle(double width, double height) => (Width, Height) = (width, height);
public double Width { get; }
public double Height { get; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
public string Describe() => $"{Width} x {Height}";
public event EventHandler? Changed;
private void Raise() => Changed?.Invoke(this, EventArgs.Empty);
}
Interface members are implicitly public and abstract; an implementing member must be public and match
exactly. A struct may implement an interface too, but be aware that converting it to the interface boxes it — see Basic Types and Variables.
An interface may also declare an indexer, and since C# 8 members may be static, private or sealed when
they have a body.
Explicit Implementation
Naming the interface in the member declaration implements it explicitly: the member is accessible only through the interface, not through the concrete type.
public interface IJsonSerializable
{
string Serialize();
}
public interface IXmlSerializable2
{
string Serialize(); // same signature, different contract
}
public sealed class Document : IJsonSerializable, IXmlSerializable2
{
// Explicit implementation disambiguates two identically-named members.
string IJsonSerializable.Serialize() => """{"kind":"document"}""";
string IXmlSerializable2.Serialize() => "<document/>";
// A public method of your own choosing can still exist alongside them.
public string ToText() => "document";
}
public static class ExplicitDemo
{
public static void Run()
{
var doc = new Document();
// doc.Serialize(); // error: no such public member
Console.WriteLine(((IJsonSerializable)doc).Serialize());
Console.WriteLine(((IXmlSerializable2)doc).Serialize());
Console.WriteLine(doc.ToText());
}
}
Use explicit implementation to resolve a name clash, or to keep a rarely-needed interface member (IDisposable
on a type where disposal is unusual, IEnumerable.GetEnumerator next to the generic one) off the type’s public
surface.
Interface Inheritance
public interface IReadable
{
string Read();
}
public interface IWritable
{
void Write(string value);
}
// An interface may extend any number of others.
public interface IReadWrite : IReadable, IWritable
{
void Clear();
}
public sealed class MemoryStore : IReadWrite
{
private string _value = "";
public string Read() => _value;
public void Write(string value) => _value = value;
public void Clear() => _value = "";
}
A type implementing IReadWrite must implement every inherited member too. Interface hierarchies are a graph,
not a tree — there is no diamond problem because interfaces (default members aside) carry no state.
Default Interface Members
Since C# 8 an interface member may have a body. The point is versioning: you can add a member to a published interface without breaking every existing implementer.
public interface ILogger2
{
void Log(string level, string message);
// Default implementations -- added later without breaking implementers.
void Info(string message) => Log("INFO", message);
void Warn(string message) => Log("WARN", message);
void Error(string message, Exception? exception = null)
=> Log("ERROR", exception is null ? message : $"{message}: {exception.Message}");
// A static member on an interface, also legal since C# 8.
static ILogger2 Null { get; } = new NullLogger2();
private sealed class NullLogger2 : ILogger2
{
public void Log(string level, string message) { }
}
}
public sealed class ConsoleLogger : ILogger2
{
// Only the one abstract member needs implementing.
public void Log(string level, string message) => Console.WriteLine($"[{level}] {message}");
}
public static class DefaultMemberDemo
{
public static void Run()
{
ILogger2 logger = new ConsoleLogger();
logger.Info("hello"); // uses the default implementation
logger.Log("DEBUG", "explicit");
// A default member is callable only through the interface, never through the class.
// new ConsoleLogger().Info("no"); // error CS1061
}
}
A default member is not inherited into the implementing class’s own surface — it is only reachable through the interface. That is the main practical difference from an abstract base class, and the reason default members are a versioning tool rather than a way to share implementation. They also let an interface act as a mixin, supplying behaviour over a small abstract core.
Static Abstract and Static Virtual Members
C# 11 allows an interface to require static members — constructors of behaviour that belong to the type rather than an instance. This unlocks generic algorithms over operators:
public interface IIdentity<TSelf> where TSelf : IIdentity<TSelf>
{
static abstract TSelf Zero { get; }
static abstract TSelf Combine(TSelf left, TSelf right);
// static virtual: a default the implementer may replace.
static virtual string Describe() => typeof(TSelf).Name;
}
public readonly record struct Meters(double Value) : IIdentity<Meters>
{
public static Meters Zero => new(0);
public static Meters Combine(Meters left, Meters right) => new(left.Value + right.Value);
}
public static class Folding
{
// The constraint gives the algorithm access to the static members.
public static T Sum<T>(IEnumerable<T> values) where T : IIdentity<T>
{
T total = T.Zero; // calling a static abstract member!
foreach (T value in values)
{
total = T.Combine(total, value);
}
return total;
}
}
The where TSelf : IIdentity<TSelf> shape is the curiously recurring pattern; it is what lets the interface
refer to the implementing type.
Generic Math
The BCL builds a whole numeric hierarchy on this. INumber<T> is the one you usually want:
public static class GenericMath
{
// Works for int, long, double, decimal, Half, BigInteger, and your own numeric types.
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T total = T.Zero;
foreach (T value in values)
{
total += value; // the operator comes from IAdditionOperators<T, T, T>
}
return total;
}
public static T Average<T>(IReadOnlyCollection<T> values) where T : INumber<T>
=> Sum(values) / T.CreateChecked(values.Count);
// A narrower constraint states exactly what you need.
public static TResult AddAll<TSelf, TOther, TResult>(TSelf seed, IEnumerable<TOther> rest)
where TSelf : IAdditionOperators<TSelf, TOther, TResult>
where TResult : TSelf
{
TSelf running = seed;
foreach (TOther value in rest)
{
running = running + value;
}
return (TResult)running;
}
public static void Demo()
{
Console.WriteLine(Sum<int>([1, 2, 3])); // 6
Console.WriteLine(Sum<double>([1.5, 2.5])); // 4
Console.WriteLine(Sum<decimal>([0.1m, 0.2m])); // 0.3
Console.WriteLine(Average<double>([1, 2, 3, 4])); // 2.5
}
}
The interfaces underneath, worth knowing by name: IAdditionOperators<TSelf, TOther, TResult> (and the
subtraction, multiplication, division and modulus equivalents), IComparisonOperators<…>,
IEqualityOperators<…>, IUnaryNegationOperators<…>, IParsable<T>, ISpanParsable<T>, IMinMaxValue<T>,
INumberBase<T> and IFloatingPoint<T>. Constrain to the narrowest one that expresses your requirement.
The Everyday BCL Interfaces
| Interface | Implement it when |
|---|---|
|
Your type owns an unmanaged or scarce resource. Enables |
|
Cleanup itself is asynchronous. Enables |
|
Your type has value equality. Avoids boxing in |
|
Your type has a natural order. Enables |
|
Your type is a sequence. Enables |
|
The sequence is produced asynchronously. Enables |
|
Your type has more than one textual representation. |
|
…and you want to format into a buffer with no allocation. |
|
Your type can be parsed from text, generically. |
|
You expose a collection callers must not modify. |
public sealed class Version3 : IEquatable<Version3>, IComparable<Version3>, IFormattable
{
public Version3(int major, int minor) => (Major, Minor) = (major, minor);
public int Major { get; }
public int Minor { get; }
public bool Equals(Version3? other)
=> other is not null && other.Major == Major && other.Minor == Minor;
public override bool Equals(object? obj) => Equals(obj as Version3);
public override int GetHashCode() => HashCode.Combine(Major, Minor);
public int CompareTo(Version3? other)
{
if (other is null)
{
return 1;
}
int byMajor = Major.CompareTo(other.Major);
return byMajor != 0 ? byMajor : Minor.CompareTo(other.Minor);
}
public override string ToString() => ToString(null, CultureInfo.CurrentCulture);
public string ToString(string? format, IFormatProvider? provider) => format switch
{
null or "G" => $"{Major}.{Minor}",
"M" => Major.ToString(provider),
_ => throw new FormatException($"Unknown format '{format}'."),
};
}
Interfaces, Abstract Classes and Delegates
| Interface | Abstract class | Delegate | |
|---|---|---|---|
Multiple per type |
Yes |
No (single inheritance) |
n/a |
Can hold state (fields) |
No |
Yes |
Captured variables |
Can have constructors |
No |
Yes |
n/a |
Default implementation |
Yes (C# 8+) |
Yes |
n/a |
Static abstract members |
Yes (C# 11+) |
No |
n/a |
Represents |
A capability |
An incomplete type |
A single operation |
The practical guidance:
-
Use an interface for a capability several unrelated types can have (
IDisposable,IComparable<T>), and for anything you want to mock or substitute in tests. It is the default choice. -
Use an abstract class when the types genuinely share implementation and state, and there is a real is-a relationship. See Inheritance and Polymorphism.
-
Use a delegate when the contract is a single operation.
Func<Order, bool>beatsIOrderPredicatealmost every time — see Delegates, Lambdas and Events.
An interface with one method is a delegate with extra ceremony; an interface with fifteen is usually several interfaces waiting to be split.
See Also
-
Inheritance and Polymorphism — the class-based alternative.
-
Generics — constraints, variance and where static abstract members fit.
-
Equality and Operator Overloading —
IEquatable<T>and operators via static abstract members. -
Collections and Iterators —
IEnumerable<T>and the collection interface hierarchy.