C# & .NET (C# 14, .NET 10 LTS) Module 7 – Inheritance, interfaces and polymorphism
Interfaces, explicit implementation and defaults
Define and implement C# interfaces, keep two contracts apart with explicit implementation, and add default and static abstract members without breaking code.
What you will learn
- Define an interface and implement several interfaces on one class
- Use explicit implementation to keep two interfaces' members apart
- Add a default member to an interface without breaking the classes that implement it
- Resolve a member that two interfaces both give a default body
- Declare static abstract members and call them from generic code
Before you start
What you’ll learn · Before you start
What you will learn
- Define an interface and implement several interfaces on one class
- Use explicit implementation to keep two interfaces' members apart
- Add a default member to an interface without breaking the classes that implement it
- Resolve a member that two interfaces both give a default body
- Declare static abstract members and call them from generic code
Before you start
On this page
A checkout offers three ways to ship a 1,200 g parcel, Rs 65 by road, Rs 200 by air and Rs 25 to a pickup point, and
it must not care which class works out each price. An interface is the contract that lets it: every class that
implements IShippingCalculator promises a Name and a Quote, and the checkout calls those and nothing else.
You will define and implement interfaces, keep two contracts apart with explicit implementation, and add a member to an interface that classes already implement.
An interface is a contract that any class can sign
An interface: A type that declares members without instance state, as a contract. A class or struct that lists it must implement its members, and a class can implement any number of interfaces. declares members but no instance fields or constructors, and a class that
lists it after the colon must implement each of its members that has no body. Interface names start with I by
convention, and their members are public by default. shipping.cs declares IShippingCalculator and three classes
that implement it, and the checkout picks one while the program runs:
// One contract, three implementations, chosen while the program runs. The rates are made up for this lesson.
IShippingCalculator[] options = [new RoadShipping(), new AirShipping(), new PickupPoint("Koramangala")];
foreach (IShippingCalculator option in options)
{
Console.WriteLine($"{option.Name,-22} Rs {option.Quote(1200),4}");
}
// The checkout knows only the interface; which class it gets is decided by the customer's choice.
IShippingCalculator chosen = Choose("air");
Console.WriteLine($"chosen: {chosen.Name}, Rs {chosen.Quote(1200)}");
// AirShipping keeps a second contract as well.
ITrackable trackable = new AirShipping();
Console.WriteLine(trackable.TrackingUrl("AW12345"));
static IShippingCalculator Choose(string code) => code switch
{
"road" => new RoadShipping(),
"air" => new AirShipping(),
_ => new PickupPoint("Koramangala"),
};
// What every shipping option promises: a name and a price in rupees for a weight in grams.
interface IShippingCalculator
{
string Name { get; }
int Quote(int grams);
}
interface ITrackable
{
string TrackingUrl(string id);
}
class RoadShipping : IShippingCalculator
{
public string Name => "Road, 4 to 6 days";
public int Quote(int grams) => 45 + ((grams - 1) / 500 * 10);
}
// One class, two interfaces, separated by commas.
class AirShipping : IShippingCalculator, ITrackable
{
public string Name => "Air, next day";
public int Quote(int grams) => 120 + ((grams - 1) / 500 * 40);
public string TrackingUrl(string id) => $"https://track.example/air/{id}";
}
class PickupPoint(string area) : IShippingCalculator
{
public string Name => $"Pickup at {area}";
public int Quote(int grams) => 25;
} Output
Road, 4 to 6 days Rs 65 Air, next day Rs 200 Pickup at Koramangala Rs 25 chosen: Air, next day, Rs 200 https://track.example/air/AW12345
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run shipping.cs
The loop sees only the interface, so a fourth way of shipping needs a new class and one more case in Choose, and
no change to the code that uses the result.
AirShipping implements a second interface, ITrackable, as well: a class has one base class but may implement any
number of interfaces. Step through what the checkout knows and what it gets:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The contract. IShippingCalculator promises two members: a Name and a Quote for a weight in grams. It has no fields and no code of its own.
- Three classes sign it. RoadShipping, AirShipping and PickupPoint each list IShippingCalculator after the colon and implement both members, each with its own rate.
- The checkout sees the contract. The loop holds each option as an IShippingCalculator and calls Quote(1200). It never names a class, yet each object answers with its own price: Rs 65, Rs 200 and Rs 25.
- Chosen at run time. Choose("air") returns an AirShipping as an IShippingCalculator. The checkout calls the same two members and gets Rs 200.
- A second contract. AirShipping also implements ITrackable. Through an ITrackable variable the same kind of object offers TrackingUrl, a member the checkout's interface does not have.
Checkpoint
Check: Interfaces as contracts, several per class
Explicit implementation keeps two contracts apart
Payment implements two interfaces that both declare Describe(): the customer’s receipt and the audit log. An
ordinary public method would serve both with one body. With explicit interface implementation: Implementing an interface member under the interface's name, as in string IReceipt.Describe(), with no access modifier. It is reached only through the interface, never through the class., the member is named with its interface, string IReceipt.Describe(), has no access
modifier, and each interface gets its own body:
// A payment tells the customer one thing and the audit log another, through two interfaces whose members are both
// called Describe. Explicit implementation gives each interface its own body.
var payment = new Payment("PAY-7781", 1499m, "asha@okbank");
IReceipt receipt = payment;
IAuditEntry audit = payment;
Console.WriteLine(receipt.Describe());
Console.WriteLine(audit.Describe());
Console.WriteLine(Print(payment));
static string Print(IReceipt receipt) => $"printer: {receipt.Describe()}";
interface IReceipt
{
string Describe();
}
interface IAuditEntry
{
string Describe();
}
class Payment(string id, decimal amount, string upiId) : IReceipt, IAuditEntry
{
// No access modifier: an explicit implementation is reached only through its interface.
string IReceipt.Describe() => $"Paid Rs {amount:F2}, reference {id}";
string IAuditEntry.Describe() => $"{id} amount={amount:F2} payer={upiId}";
} Output
Paid Rs 1499.00, reference PAY-7781 PAY-7781 amount=1499.00 payer=asha@okbank printer: Paid Rs 1499.00, reference PAY-7781
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run receipts.cs
The receipt leaves out the payer’s ID, which only the audit text includes, and Print reaches the receipt’s version
because it takes an IReceipt. An explicit implementation is reached only through its interface, never through the
class:
// receipts.cs, with one more line: Describe called on the Payment itself.
var payment = new Payment("PAY-7781", 1499m, "asha@okbank");
Console.WriteLine(payment.Describe());
interface IReceipt
{
string Describe();
}
interface IAuditEntry
{
string Describe();
}
class Payment(string id, decimal amount, string upiId) : IReceipt, IAuditEntry
{
string IReceipt.Describe() => $"Paid Rs {amount:F2}, reference {id}";
string IAuditEntry.Describe() => $"{id} amount={amount:F2} payer={upiId}";
} from session import run
# explicitcall calls Describe on a Payment variable, although Payment implements Describe only explicitly. dotnet run
# builds the project first and stops at the error.
run("""
cd explicitcall
!dotnet run
""") Output
$ cd explicitcall $ dotnet run explicitcall/Program.cs(3,27): error CS1061: 'Payment' does not contain a definition for 'Describe' and no accessible extension method 'Describe' accepting a first argument of type 'Payment' could be found (are you missing a using directive or an assembly reference?) [explicitcall/App.csproj] The build failed. Fix the build errors and run again. [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 explicit_session.py
CS1061 says that Payment has no Describe at all. Explicit implementation also hides a member from the class’s
public surface on purpose, as an IDisposable.Dispose written explicitly does.
Checkpoint
Check: Explicit interface implementation
Interface defaults let you add a member without breaking implementers
Adding an ordinary member to a published interface breaks every class that implements it: none of them implements
the new member, so none of them compiles, as a class that skips an abstract member fails with CS0534. Interface
defaults, default interface member: An interface member with a body, allowed since C# 8 on .NET Core 3.0 and later. Classes that do not implement the member use the body; it is called through the interface only. added in C# 8, avoid that: a member with a body
is used by every class that does not implement it itself. defaults.cs adds Describe to IShippingCalculator:
// Version 2 of IShippingCalculator adds Describe with a default body. RoadShipping and PickupPoint were written for
// version 1 and compile unchanged; AirShipping writes a Describe of its own.
IShippingCalculator[] options = [new RoadShipping(), new AirShipping(), new PickupPoint("Koramangala")];
foreach (IShippingCalculator option in options)
{
Console.WriteLine(option.Describe(1200));
}
interface IShippingCalculator
{
string Name { get; }
int Quote(int grams);
// New in version 2: a default implementation, for every class that does not write its own.
string Describe(int grams) => $"{Name}: Rs {Quote(grams)} for {grams} g";
}
// Written for version 1.
class RoadShipping : IShippingCalculator
{
public string Name => "Road, 4 to 6 days";
public int Quote(int grams) => 45 + ((grams - 1) / 500 * 10);
}
// Written for version 2: its own public Describe implements the interface member.
class AirShipping : IShippingCalculator
{
public string Name => "Air, next day";
public int Quote(int grams) => 120 + ((grams - 1) / 500 * 40);
public string Describe(int grams) => $"{Name}: Rs {Quote(grams)}, tracked to your door";
}
// Written for version 1.
class PickupPoint(string area) : IShippingCalculator
{
public string Name => $"Pickup at {area}";
public int Quote(int grams) => 25;
} Output
Road, 4 to 6 days: Rs 65 for 1200 g Air, next day: Rs 200, tracked to your door Pickup at Koramangala: Rs 25 for 1200 g
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run defaults.cs
RoadShipping and PickupPoint were written for version 1 and use the default; AirShipping’s own public
Describe takes its place. Like an explicit implementation, a default belongs to the interface, not to the class:
// defaults.cs, with Describe called on a RoadShipping variable instead of an IShippingCalculator one.
var road = new RoadShipping();
Console.WriteLine(road.Describe(1200));
interface IShippingCalculator
{
string Name { get; }
int Quote(int grams);
string Describe(int grams) => $"{Name}: Rs {Quote(grams)} for {grams} g";
}
class RoadShipping : IShippingCalculator
{
public string Name => "Road, 4 to 6 days";
public int Quote(int grams) => 45 + ((grams - 1) / 500 * 10);
} from session import run
# defaultcall calls the interface's default Describe through a RoadShipping variable. dotnet run builds the project
# first and stops at the error.
run("""
cd defaultcall
!dotnet run
""") Output
$ cd defaultcall $ dotnet run defaultcall/Program.cs(3,24): error CS1061: 'RoadShipping' does not contain a definition for 'Describe' and no accessible extension method 'Describe' accepting a first argument of type 'RoadShipping' could be found (are you missing a using directive or an assembly reference?) [defaultcall/App.csproj] The build failed. Fix the build errors and run again. [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 default_session.py
Two more features build on this. When two interfaces both supply a body for one member, the compiler needs a rule to pick one. And a static abstract member: A static member that an interface declares and every implementing type must supply, allowed since C# 11. Generic code calls it on a type parameter, as T.Parse(text). puts a contract on the type itself rather than on its objects. Generic code can call it on a type parameter before it has any object of that type.
Default members need .NET Core 3.0 or later
Microsoft’s history of C# says default interface members need support in the runtime that was added for .NET Core 3.0, so .NET Framework cannot run them, whatever the language version.
Checkpoint
Check: Default interface members
Defaults in depth: which body runs, the diamond, and what a default cannot do Pro: opens with a Pro, Premium or Ultimate pass
A member with a default can get its body from three places, and two interfaces can offer two bodies for one member. The runs below show the order the compiler applies, the error when it cannot decide, and the fix.
In this part:
- The class’s method first, then the most specific interface
- Two equally specific bodies: CS8705, reproduced and fixed
- What a default cannot do
3 runnable examples (C#, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Static abstract members: a contract on the type itself, run in generic code Pro: opens with a Pro, Premium or Ultimate pass
An ordinary interface member promises something about each object; a static abstract member promises something about the type. That lets generic code call a static method of whatever type it is given.
In this part:
- A loader that creates any type from CSV, through T.FromCsv
- Chosen by the compiler, not by the object
1 runnable example (C#) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interfaces as seams: a test double in a run, and Java's interfaces compared Pro: opens with a Pro, Premium or Ultimate pass
An interface between two classes is a seam: either side can be replaced without touching the other. The run below replaces an SMS gateway with a fake that a test can read, and the table maps C# interfaces to Java’s for readers who know both.
In this part:
- A fake gateway that records what would have been sent
- When an interface is ceremony
- C# interfaces next to Java’s
1 runnable example (C#) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: fare rules behind one interface
fares.cs checks three fare rules behind IFareRule and its default Describe. Every rule that applies adjusts the
fare in turn, in paise, rounded down. All 8 checks fail on the starter:
// Practice: write three fare rules and the default Describe of IFareRule, then run this file until all eight checks
// pass. The percentages are made up for this exercise and are no railway's or bus company's real rules.
IFareRule[] rules = [new SeniorCitizenRule(), new ChildRule(), new PeakHourRule()];
(string Check, Func<string> Run, string Expected)[] checks =
[
("a senior citizen pays 40 % less", () => Apply(new Trip(10_000, 64, 13)), "6000"),
("a child pays half", () => Apply(new Trip(10_000, 8, 13)), "5000"),
("an adult off peak pays the fare", () => Apply(new Trip(10_000, 30, 13)), "10000"),
("peak hours add 10 %", () => Apply(new Trip(10_000, 30, 9)), "11000"),
("a senior citizen at peak hours", () => Apply(new Trip(10_000, 64, 18)), "6600"),
("paise are rounded down", () => Apply(new Trip(9_999, 64, 13)), "5999"),
("Describe has a default", () => rules[1].Describe(new Trip(10_000, 30, 13)), "Child fare does not apply"),
("PeakHourRule implements Adjust explicitly", () => (typeof(PeakHourRule).GetMethod("Adjust") is null).ToString(), "True"),
];
int passed = 0;
foreach (var (check, run, expected) in checks)
{
string got;
try
{
got = run();
}
catch (Exception e)
{
got = $"throws {e.GetType().Name}";
}
bool ok = got == expected;
if (ok)
{
passed++;
}
Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;
// Every rule that applies adjusts the fare, in the order of the rules array.
string Apply(Trip trip) => rules.Where(rule => rule.Applies(trip)).Aggregate(trip.FarePaise, (fare, rule) => rule.Adjust(fare)).ToString();
record Trip(int FarePaise, int PassengerAge, int Hour);
interface IFareRule
{
string Name { get; }
bool Applies(Trip trip);
// The fare in paise after this rule, rounded down to whole paise.
int Adjust(int farePaise);
// Give this member a default body: "<Name> applies" or "<Name> does not apply".
string Describe(Trip trip) => throw new NotImplementedException();
}
// "Senior citizen fare": passengers aged 60 or more pay 40 % less.
class SeniorCitizenRule : IFareRule
{
public string Name => throw new NotImplementedException();
public bool Applies(Trip trip) => throw new NotImplementedException();
public int Adjust(int farePaise) => throw new NotImplementedException();
}
// "Child fare": passengers under 12 pay half.
class ChildRule : IFareRule
{
public string Name => throw new NotImplementedException();
public bool Applies(Trip trip) => throw new NotImplementedException();
public int Adjust(int farePaise) => throw new NotImplementedException();
}
// "Peak hours": trips that start from 8 to 10 or from 17 to 19 o'clock cost 10 % more. Implement Adjust explicitly.
class PeakHourRule : IFareRule
{
public string Name => throw new NotImplementedException();
public bool Applies(Trip trip) => throw new NotImplementedException();
public int Adjust(int farePaise) => throw new NotImplementedException();
} Output (exit status 1)
a senior citizen pays 40 % less: FAIL, throws NotImplementedException instead of 6000 a child pays half: FAIL, throws NotImplementedException instead of 5000 an adult off peak pays the fare: FAIL, throws NotImplementedException instead of 10000 peak hours add 10 %: FAIL, throws NotImplementedException instead of 11000 a senior citizen at peak hours: FAIL, throws NotImplementedException instead of 6600 paise are rounded down: FAIL, throws NotImplementedException instead of 5999 Describe has a default: FAIL, throws NotImplementedException instead of Child fare does not apply PeakHourRule implements Adjust explicitly: FAIL, False instead of True 0 of 8 checks pass
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run fares.cs
Write the rules and the default
Copy fares.cs. Give each rule its Name, its condition in Applies and its sum in Adjust (farePaise * 60 / 100 for 40 % off). Implement PeakHourRule’s Adjust explicitly, as int IFareRule.Adjust(int farePaise), and
replace the body of Describe in the interface. Run dotnet run fares.cs until it prints 8 of 8 checks pass.
Then practise on contracts, explicit implementation and defaults:
Practice that adapts to you
Practise this lesson
7 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is explicit interface implementation, and when do you use it?
It is implementing an interface member under the interface’s name, void IAuditEntry.Write(), with no access
modifier, so the member is reached only through a variable of that interface type and never through the class. Use
it when two interfaces declare members with the same signature that need different bodies, such as a customer’s
receipt and an audit entry that both have Describe(). Use it also to keep an interface member off the class’s public
surface, when callers should reach it only through the interface. The cost is that code holding the class must cast
to the interface first, so keep it for those two cases.
-
Mid to senior
Many teams implement your library's public interface IPaymentGateway, which has Charge. You now need Refund. Compare a default member, a new interface IRefundableGateway, and an extension method, and say which you would ship and why.
-
Mid to senior
After you upgrade two libraries, a class that implements one interface from each stops compiling with CS8705: both interfaces now give the same inherited member a default body. Explain why the compiler refuses, how you fix your class, and what library authors can do to avoid it.
-
Mid to senior
A codebase has an interface for every class, IOrderService for OrderService and so on, each with one implementation, and a colleague says testing and dependency injection need them. When is an interface worth having, and when is it ceremony? How would you decide for a given class?
Key takeaways
- An interface declares members without instance state, and a class implements any number of interfaces, so code that depends on the interface accepts every class that keeps the contract.
- Explicit implementation gives each interface its own body for a same-named member and keeps the member off the class: CS1061 refuses the call through the class.
- A default member lets an interface gain a member without breaking implementers; it is used through the interface only, and needs .NET Core 3.0 or later.
Terms in this lesson
- default interface member
- An interface member with a body, allowed since C# 8 on .NET Core 3.0 and later. Classes that do not implement the member use the body; it is called through the interface only.
- explicit interface implementation
- Implementing an interface member under the interface's name, as in string IReceipt.Describe(), with no access modifier. It is reached only through the interface, never through the class.
- interface
- A type that declares members without instance state, as a contract. A class or struct that lists it must implement its members, and a class can implement any number of interfaces.
- static abstract member
- A static member that an interface declares and every implementing type must supply, allowed since C# 11. Generic code calls it on a type parameter, as T.Parse(text).
References
- Interfaces: define behavior for multiple types (Microsoft Learn)
- Explicit interface implementation (Microsoft Learn)
- Safely update interfaces using default interface methods (Microsoft Learn)
- interface keyword (Microsoft Learn)
- The history of C# (Microsoft Learn)
- .NET API changes that affect compatibility (Microsoft Learn)
- IParsable<TSelf> interface (Microsoft Learn)
- Unit testing best practices for .NET (Microsoft Learn)
- Service registration (dependency injection) (Microsoft Learn)
- The Java Language Specification, Java SE 25: interfaces (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress