C# Interfaces: Default Methods and Static Abstract
C# interfaces on .NET 10: explicit implementation, C# 8 default methods, C# 11 static abstract members with real compiler errors, vs abstract classes.
Revised November 2026. This tutorial first appeared in Korean in 2021. Both of its examples still compile and print the same output on .NET 10.0.14 (C# 14), but five statements in the text were wrong or out of date. The biggest one, "implementing a shared method name without the interface name is an error", was not true even in 2021. Since C# 8, interfaces can have method bodies, static fields and private members, and since C# 11 they can declare static abstract members for generic math. Every example below was compiled, and the compiler errors are quoted with their codes.
This article is for developers who learned C# interfaces as "a list of methods without bodies" and are now reading code with static abstract members or interface methods that have bodies. The short version: an interface is still a contract that says what a type can do, but in 2026 it can carry default behavior and static contracts as well. If you need instance state, use an abstract class; for almost everything else, an interface is the better fit.
Interface basics, and what the 2021 version got wrong
You declare an interface with interface, and a class or struct implements it with : IName. The original article called that "inheriting"; it is clearer to say classes inherit from classes and implement interfaces. A class has one base class, but it can implement any number of interfaces.
We checked each item of the original feature list on .NET 10:
| 2021 statement | What .NET 10 / C# 14 actually does |
|---|---|
| Interfaces can contain methods, events, indexers and properties | True, plus static members, operators and nested types |
| Interfaces cannot contain fields | Only instance fields are banned (CS0525). Static fields are allowed (C# 8) |
| All members are public by default | Still the default, but private, protected and internal can be written explicitly (C# 8) |
| Members are abstract, with no body | Since C# 8, members can have a default implementation |
| A class can inherit one interface several times | Listing the same interface twice is CS0528. The correct statement is "a class can implement several interfaces" |
| An interface is a kind of class | It is a separate kind of type and cannot be instantiated (CS0144) |
The actual compiler messages:
error CS0525: Interfaces cannot contain instance fields
error CS0528: 'IA' is already listed in interface list
The original interface-inheritance example (MyClass implements SubInterface, which extends ParentInterface) runs unchanged:
Interface ParentInterface.myMethod() call!
Inherits SubInterface.myMethod() 0 call!
Inherits SubInterface.myMethod() 1 call!
Inherits SubInterface.myMethod() 2 call!
Inherits SubInterface.myMethod() 3 call!
The original rule that "every member must be implemented" still holds for members without a body. Members with a default implementation, covered below, can be left out.
Explicit implementation: when you actually need it
The first original example implemented two interfaces that both declare print() explicitly, as void IMyInterfaceA.print() and void IMyInterfaceB.print(). The output is the same as in 2021:
IMyInterfaceA.print() called.
IMyInterfaceB.print() called.
The article then claimed that writing the method without the interface name would be an error, because the compiler could not tell which interface it belongs to. In practice it compiles:
var mc = new MyClass();
IMyInterfaceA a = mc;
IMyInterfaceB b = mc;
a.print(); b.print(); mc.print();
interface IMyInterfaceA { void print(); }
interface IMyInterfaceB { void print(); }
class MyClass : IMyInterfaceA, IMyInterfaceB
{
public void print() => Console.WriteLine("one public print() serves both");
}
one public print() serves both
one public print() serves both
one public print() serves both
One public method satisfies both contracts. Explicit implementation is the right tool when:
- each interface needs different behavior, as in the original example;
- you must expose two members that differ only in return type. Implementing
IEnumerable<T>and explicitly implementing the non-genericIEnumerable.GetEnumerator()is the classic case; - you want to keep a member off the class's public surface. An explicitly implemented member is invisible through the class type:
var e = new Explicit();
e.print(); // compile error
((IMyInterfaceA)e).print(); // OK
class Explicit : IMyInterfaceA { void IMyInterfaceA.print() => Console.WriteLine("explicit only"); }
error CS1061: 'Explicit' does not contain a definition for 'print' and no accessible extension method 'print' accepting a first argument of type 'Explicit' could be found (are you missing a using directive or an assembly reference?)
One caution: if a struct has to be cast to an interface to reach an explicit member, that cast boxes the struct. We measured the allocations in the structs article.
Default interface methods (C# 8)
Since C# 8, an interface member can have a body. The feature exists so you can add a method to a published interface without breaking every class that already implements it.
ILogger plain = new ConsoleLogger();
ILogger custom = new LoudLogger();
plain.LogError("disk full");
custom.LogError("disk full");
Console.WriteLine($"ILogger.Count = {ILogger.Count}");
interface ILogger
{
static int Count; // static field: allowed
void Log(string message);
void LogError(string message) => Log(Stamp("ERROR " + message)); // default implementation
private string Stamp(string s) { Count++; return $"[{Count}] {s}"; } // private member
}
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) => Log("!!! " + message.ToUpper());
}
[1] ERROR disk full
!!! DISK FULL
ILogger.Count = 1
ConsoleLogger does not implement LogError, so the default body ran. LoudLogger has a public method with the same signature, and that method took precedence. The detail that trips people up in real projects: default implementations are not inherited by the class.
var log = new ConsoleLogger();
log.LogError("x"); // not visible through ConsoleLogger
error CS1061: 'ConsoleLogger' does not contain a definition for 'LogError' and no accessible extension method 'LogError' accepting a first argument of type 'ConsoleLogger' could be found (…)
The default body is only visible when you hold the object as ILogger. If callers use the class type, implement the method on the class or write an extension method for the interface instead. Compiling with LangVersion 7.3 gives error CS8370: Feature 'default interface implementation' is not available in C# 7.3. Please use language version 8.0 or greater., which confirms it is a C# 8 feature. It also needs runtime support: according to the Microsoft Learn documentation it is not available on .NET Framework and requires .NET Core 3.0 or later.
Static abstract members and generic math (C# 11)
Since C# 11, an interface can require static members and operators as part of the contract. That is what lets a generic method write T.Zero or a + b.
using System.Numerics;
Console.WriteLine(Total<Money>([new(100), new(250), new(5)]));
Console.WriteLine(Total2([1.5, 2.25]));
static T Total<T>(IEnumerable<T> items) where T : IAddable<T>
{
T sum = T.Zero;
foreach (var x in items) sum = sum + x;
return sum;
}
static T Total2<T>(IEnumerable<T> items) where T : INumber<T>
{
T sum = T.Zero;
foreach (var x in items) sum += x;
return sum;
}
interface IAddable<T> where T : IAddable<T>
{
static abstract T Zero { get; }
static abstract T operator +(T a, T b);
}
readonly record struct Money(decimal Amount) : IAddable<Money>
{
public static Money Zero => new(0);
public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
}
Money { Amount = 355 }
3.75
Three things we confirmed while compiling this:
- You cannot pass
intto your ownIAddable<T>.Total<int>([1, 2, 3])fails witherror CS0315: The type 'int' cannot be used as type parameter 'T' in the generic type or method 'Total<T>(IEnumerable<T>)'. There is no boxing conversion from 'int' to 'IAddable<int>'.You cannot attach an interface to an existing type, so for numbers use theINumber<T>family that .NET already implements (Total2). - Static abstract members are reachable only through a type parameter. Calling
IZero.Zeroon the interface itself giveserror CS8926: A static virtual or abstract interface member can be accessed only on a type parameter. - Compiling with
LangVersion=10giveserror CS8703: The modifier 'abstract' is not valid for this item in C# 10.0. Please use language version '11.0' or greater., which confirms the C# 11 claim.
The basics of where T : … constraints are in our generics article.
Interface vs abstract class: a decision table
Now that interfaces can have bodies, people often ask what is left to separate them from abstract classes. Quite a lot:
| Criterion | Interface | Abstract class |
|---|---|---|
| How many per type | Any number | One base class |
| Usable by structs | Yes | No (structs cannot inherit) |
| Instance fields (state) | No (CS0525) |
Yes |
| Constructors | No | Yes, chained with : base(…) |
| Default behavior | Yes (C# 8), visible only through the interface type | Yes, visible through derived types too |
Static contracts (operators, T.Zero) |
static abstract (C# 11) | No |
| Adding members later | Compatible if they have a default body | Compatible for non-abstract members |
| Access modifiers | Public by default, others allowed | Anything |
A common pattern in production code is to publish the contract as an interface and, if implementations share a lot of code, add an abstract base class that implements that interface. Consumers depend only on the interface, which keeps test doubles easy to plug in, while implementers inherit the shared code from the abstract class. For virtual/override, sealed and the virtual-call-in-constructor trap, see class inheritance.
Tested with .NET SDK 10.0.403 / runtime 10.0.14, C# 14 (the net10.0 default), macOS on an Apple Silicon Mac. Compiler messages are the English SDK output.
FAQ
If two interfaces declare the same method, do I have to implement them explicitly?
No. A single public method satisfies both interfaces and compiles fine. The 2021 version of this article said it would be an error without the interface name, but .NET 10 reports no error. Use explicit implementation, such as void IMyInterfaceA.print(), only when each interface needs different behavior.
Can I call a default interface method (C# 8) through a class-typed variable?
No. A default method that the class does not implement itself is only visible through the interface type. Calling LogError on a ConsoleLogger variable gave error CS1061; calling it on an ILogger variable ran the default body. If the class has a public method with the same signature, that one wins.
What are static abstract interface members for?
They let generic code use static members and operators of the type itself, such as T.Zero or a + b (C# 11). .NET's INumber<T> is built on them, so it works with int and double out of the box. Calling IZero.Zero through the interface name gives error CS8926; they are only reachable through a type parameter.