Interfaces: Explicit Implementation and Default Methods
Objective
An interface is a contract: a type that implements it promises to have certain
members, and code that depends on the interface can work with any of those
types, classes and structs alike. Modern C# interfaces carry more than
signatures. They can hide a member behind explicit implementation, ship a
default body (C# 8), and require static members such as factories and operators
(C# 11). This concept covers implicit versus explicit implementation and when
explicit is the only option, default interface methods and the diamond they can
create, static abstract and static virtual members at an introductory level,
and the comparison trio every data type meets sooner or later: IComparable<T>,
IComparer<T> and IEquatable<T>. The choice between an interface and an
abstract class is covered in the inheritance concept.
Use Cases
- A class that is both a writer and a connection, where
Closemeans a different thing for each. - Implementing
IEnumerable<T>without exposing the weakly typed non-genericGetEnumeratoron the public surface. - Adding a convenience method to a published interface without breaking every existing implementation.
- A generic
ParseAll<T>that needs to callT.Parseon the type itself, not on an instance. - Making a value type sortable with
Array.Sortand usable as aHashSetkey without surprises.
Deep Dive
Implicit and explicit implementation
A public member with the right signature implements the interface member implicitly, and callers can reach it through the class or the interface. An explicit implementation names the interface and is reachable only through an interface-typed reference:
csharpvar file = new LogFile();
((IWriter)file).Close(); // flush buffer
((IConnection)file).Close(); // drop socket
// file.Close(); // error CS1061: 'LogFile' does not contain a definition for 'Close'
interface IWriter { void Close(); }
interface IConnection { void Close(); }
sealed class LogFile : IWriter, IConnection
{
void IWriter.Close() => Console.WriteLine("flush buffer");
void IConnection.Close() => Console.WriteLine("drop socket");
}Explicit implementation is required when two interfaces declare the same
signature with different meanings, or when two members differ only in return
type, which is exactly the case of IEnumerable<T>: the generic and the
non-generic GetEnumerator() take no parameters and return different types.
The usual pattern is to implement the generic one publicly and forward the old
one explicitly:
csharpsealed class Names(string[] items) : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator() => ((IEnumerable<string>)items).GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}It is also a way to keep a member off the public API when it only makes sense to
code that already treats the object as the interface. Explicit members cannot
have an access modifier and cannot be virtual; a subclass that needs to change
them re-implements the interface.
Default interface methods (C# 8)
An interface member can have a body. Classes that do not implement the member get the default; classes that do implement it replace it:
csharpILogger log = new Plain();
log.Info("started"); // INFO started
ILogger custom = new Prefixed();
custom.Info("started"); // [info] STARTED
interface ILogger
{
void Write(string level, string message);
void Info(string message) => Write("INFO", message); // added in v2
}
class Plain : ILogger
{
public void Write(string level, string message) => Console.WriteLine($"{level} {message}");
}
class Prefixed : ILogger
{
public void Write(string level, string message) => Console.WriteLine($"[{level}] {message}");
public void Info(string message) => Write("info", message.ToUpperInvariant());
}Three things surprise people. First, the default is not inherited as a class
member: new Plain().Info("x") fails with CS1061, so you must call it through
the interface. Second, default bodies need runtime support that arrived in
.NET Core 3.0, which is why .NET Framework cannot use them. Third, the main
motivation is versioning: the Framework Design Guidelines still say not to add
members to a shipped interface, and a default body is what makes adding one
non-breaking for existing implementers.
Interfaces still cannot hold instance state. A property declared in an interface without a body is abstract, not an auto-property, because there is no field to put the value in.
The diamond
If two interfaces derived from the same base both supply a body for the same member, a class that implements both has no single answer, and the compiler refuses:
csharpinterface IShape { string Name() => "shape"; }
interface IRounded : IShape { string IShape.Name() => "rounded"; }
interface IFramed : IShape { string IShape.Name() => "framed"; }
class Square : IRounded, IFramed { }
// error CS8705: Interface member 'IShape.Name()' does not have a most specific implementation.
// Neither 'IRounded.IShape.Name()', nor 'IFramed.IShape.Name()' are most specific.The fix is to implement the member in the class, which is always the most specific. Unlike multiple class inheritance in C++, there is no state that could be duplicated, only an ambiguity in which body to run, and C# turns that into a compile error instead of a rule you must memorize.
static abstract and static virtual members (C# 11)
An interface can require a static member. The implementing type provides it, and generic code calls it through the type parameter:
csharpusing System.Globalization;
Console.WriteLine(ParseAll<Celsius>(["21", "30"]).Sum(c => c.Value)); // 51
Console.WriteLine(Describe<Celsius>()); // unit=C
static List<T> ParseAll<T>(string[] raw) where T : IParseableUnit<T>
=> raw.Select(T.Parse).ToList();
static string Describe<T>() where T : IParseableUnit<T> => $"unit={T.Unit}";
interface IParseableUnit<TSelf> where TSelf : IParseableUnit<TSelf>
{
static abstract TSelf Parse(string text);
static virtual string Unit => "?"; // has a default
}
readonly record struct Celsius(double Value) : IParseableUnit<Celsius>
{
public static Celsius Parse(string text) => new(double.Parse(text, CultureInfo.InvariantCulture));
public static string Unit => "C";
}The TSelf : IParseableUnit<TSelf> constraint is the self-referencing pattern
the BCL uses everywhere (IParsable<TSelf>, INumber<TSelf>). The official
reference stresses that these calls have no runtime dispatch like instance
virtual methods: they resolve through the type the generic code was given, not
through an object's runtime type. This feature is what makes generic math
possible; that topic gets its own concept.
IComparable<T>, IComparer<T> and IEquatable<T>
IComparable<T> gives a type its natural order. IComparer<T> is an order
defined outside the type, so one type can be sorted in many ways.
IEquatable<T> gives a strongly typed Equals:
csharpSemVer[] versions = [new(2, 0), new(1, 10), new(1, 2)];
Array.Sort(versions); // 1.2, 1.10, 2.0
Array.Sort(versions, Comparer<SemVer>.Create((a, b) => a.Minor.CompareTo(b.Minor)));
// 2.0, 1.2, 1.10
var set = new HashSet<SemVer> { new(1, 2), new(1, 2) }; // Count: 1
readonly struct SemVer(int major, int minor) : IComparable<SemVer>, IEquatable<SemVer>
{
public int Major => major;
public int Minor => minor;
public int CompareTo(SemVer other) =>
Major != other.Major ? Major.CompareTo(other.Major) : Minor.CompareTo(other.Minor);
public bool Equals(SemVer other) => Major == other.Major && Minor == other.Minor;
public override bool Equals(object? obj) => obj is SemVer v && Equals(v);
public override int GetHashCode() => HashCode.Combine(Major, Minor);
public override string ToString() => $"{Major}.{Minor}";
}EqualityComparer<T>.Default, used by HashSet<T> and Dictionary<TKey,TValue>,
checks whether T implements IEquatable<T> and uses it when it does (for this
struct it returned a GenericEqualityComparer<SemVer>); otherwise it falls
back to object.Equals. Implementing IEquatable<T> therefore obliges you to
override Equals(object) and GetHashCode consistently, or different
collections will disagree.
Trade-offs
- Explicit implementation hides members from your own users. It is right for
name clashes and legacy members, but a member people need should not require
a cast to find.csharp
((IConnection)file).Close(); // fine for a rare clash, annoying for a daily call - Default methods version interfaces, they do not replace base classes. No state, no constructors, and callers must hold the interface type to see the default. Keep defaults small and written in terms of the abstract members.
- Adding a default body can still change behavior. An existing class that
happened to have a public method with the same signature silently becomes the
implementation.csharp
class Old : ILogger { public void Write(string level, string message) { } public void Info(string message) { } // older, unrelated method: now it is ILogger.Info } static abstractlimits how the interface is used. It is a constraint for generic code, not a polymorphic type you pass around, and it pulls in the self-referencing generic pattern that is harder to read.IComparable<T>fixes one order for everybody. If there is no obvious natural order, do not implement it; pass anIComparer<T>where you sort.Array.Sortis not stable. The documentation says elements that compare equal may come out in any order, which matters as soon as a custom comparer looks at only part of the value. UseOrderBy(stable) when the original order of ties matters.csharpvar sorted = versions.OrderBy(v => v.Minor).ToArray(); // ties keep their input order
Documentation Links
- interface keyword, C# reference, Microsoft Learn
- Explicit interface implementation, Microsoft Learn
- Tutorial: Update interfaces with default interface methods, Microsoft Learn
- Tutorial: Explore static virtual members in interfaces, Microsoft Learn
- Interface design, Framework Design Guidelines, Microsoft Learn
- EqualityComparer<T>.Default, Microsoft Learn
- IComparable<T> interface, Microsoft Learn
- IComparer<T> interface, Microsoft Learn
- IEquatable<T> interface, Microsoft Learn