Interfaces in C#
An interface defines a contract — a set of members (methods, properties, events, indexers) that any implementing type promises to provide — without dictating how those members are implemented or how the type is otherwise structured. Interfaces answer the question 'what can you do?' rather than 'what are you?', which is why unrelated types like FileLogger, DatabaseLogger, and ConsoleLogger can all implement 'ILogger' despite sharing no common ancestor beyond object. A class or struct can implement any number of interfaces, which is how C# achieves multiple-inheritance-like flexibility while avoiding the ambiguity of multiple class inheritance.
Cricket analogy: A 'can bowl an over' contract lets a part-time spinner, a genuine fast bowler, and an occasional medium-pacer all be called upon despite having completely different bowling actions and backgrounds.
Declaring and implementing an interface
By convention, interface names begin with a capital 'I' ('IDisposable', 'IEnumerable<T>'). Historically, every interface member was implicitly abstract and public, with no implementation and no accessibility modifier allowed; implementing types had to supply a public member for each one. A class implements an interface using the same colon syntax as inheritance, and can list a base class and multiple interfaces together.
Cricket analogy: Just as every certified umpire must wear the recognizable white coat by convention, every interface name starting with 'I' signals its role at a glance, and historically every listed duty was mandatory with no shortcuts.
public interface IShippable
{
decimal CalculateShippingCost(double distanceKm);
}
public interface ITrackable
{
string TrackingNumber { get; }
}
public class Package : IShippable, ITrackable
{
public string TrackingNumber { get; } = Guid.NewGuid().ToString("N")[..10];
public decimal CalculateShippingCost(double distanceKm) =>
(decimal)(distanceKm * 0.15);
}Default interface methods and explicit implementation
Since C# 8, an interface can provide a default (concrete) method body, letting the interface evolve — adding a new member — without breaking every existing implementer, since types that don't override it simply inherit the default behavior. Separately, explicit interface implementation ('void ILogger.Log(...)') hides a member so it's only accessible through a variable typed as the interface, not through the concrete class — useful for resolving name collisions between two interfaces, or for keeping an implementation detail out of a class's primary public surface.
Cricket analogy: Since the DRS rules were updated, a new 'soft signal' default behavior applies automatically to matches that haven't opted into the newer protocol, while a scorer's private notation stays hidden from broadcast viewers.
public interface ILogger
{
void Log(string message);
void LogError(string message) => Log($"ERROR: {message}"); // default method
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
// LogError inherited from the interface's default implementation
}Interfaces vs. abstract classes
Interfaces and abstract classes both express 'what a type can do', but an abstract class can also hold shared state (fields), constructors, and non-public members, while a class can only inherit from a single abstract class yet implement many interfaces. Prefer interfaces when unrelated types need a shared capability with little or no shared implementation; prefer an abstract base class when derived types genuinely share both behavior and state, and a true single-inheritance 'is-a' relationship makes sense.
Cricket analogy: An all-rounder like Ravindra Jadeja can bat and bowl (many interfaces), but a specialist keeper-batter role with inherited technique and shared training history (an abstract base) only comes from one wicketkeeping lineage.
Default interface methods bring C# closer to Java's default methods (Java 8+) and to how Swift protocol extensions work, letting API authors add members to widely-implemented interfaces (like IEnumerable) without a breaking change.
A common pitfall: calling an explicitly-implemented interface member on a variable declared as the concrete class type fails to compile, because explicit implementations are only visible through the interface type — you must cast or declare the variable as the interface.
- Interfaces define a contract of members without dictating implementation; classes and structs can implement any number of them.
- Historically all interface members were implicitly public and abstract; C# 8 added default (concrete) interface methods.
- Explicit interface implementation hides a member from the concrete type, exposing it only via the interface type.
- A class can implement multiple interfaces but inherit from only one base class.
- Interfaces express capability ('can do'); abstract classes can additionally share state and implementation ('is a').
- Default methods let interfaces evolve without breaking existing implementers.
Practice what you learned
1. How many interfaces can a single C# class implement?
2. What feature, introduced in C# 8, lets an interface provide a concrete method body?
3. How is an explicitly-implemented interface member accessed?
4. Which of these can an abstract class have that a classic interface (pre-C# 8) could not?
5. Why might an API author add a default interface method instead of just adding an abstract method to an existing interface?
Was this page helpful?
You May Also Like
Abstract Classes
Learn how abstract classes provide a partially-implemented base type that cannot be instantiated directly, mixing shared implementation with members derived classes must supply.
Polymorphism Explained
See how C# achieves polymorphism through virtual method dispatch, interface implementations, and method/operator overloading, letting one code path handle many shapes of data.
Inheritance in C#
Learn how C# classes derive from a single base class to reuse and extend behavior, including virtual/override methods, base member access, and sealing.
Generics Explained
Understand how C# generics let you write type-safe, reusable code that works across many types without sacrificing performance or requiring casts.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics