Most developers are familiar with the differences between abstract classes and interfaces—at least, I hope so!
At a high level, the key differences are:
Abstract Classes:
- Cannot be instantiated
- Must be inherited by other classes
- Can contain abstract and non-abstract methods
- Can include fields and properties
- Do not support multiple inheritance
- Can have constructors
Interfaces:
- Support multiple inheritance
- Can have only default implementations (from C# 8.0 onward)
- Do not have fields or properties (except static or default implementations)
- Cannot have constructors
- Use a separate table (ITable) for method dispatch
While these differences are visible on the surface, the real distinction lies in how the Common Language Runtime (CLR) handles abstract classes and interfaces under the hood. Let's dive deeper into these internal differences.
A Practical Example
Consider the following code snippet demonstrating an abstract class and an interface:
public abstract class SomeAbstractClass
{
public abstract void Print();
}
public interface ISomeInterface
{
public void Print();
}
public class ConcreteClass1 : SomeAbstractClass
{
public override void Print()
{
Console.WriteLine("This is concrete class 1");
}
}
public class ConcreteClass2 : ISomeInterface
{
public void Print()
{
Console.WriteLine("This is concrete class 2");
}
}
var class1 = new ConcreteClass1();
class1.Print();
var class2 = new ConcreteClass2();
class2.Print();
How the CLR Handles Abstract Classes
When we call the Print() method on an abstract class, the CLR uses the callvirt
instruction. This instruction looks up the virtual method table (vtable) and calls the correct implementation:
IL_0006: ldloc.0 // class1
IL_0007: callvirt instance void AbstractVsInterface.SomeAbstractClass::Print()
IL_000c: nop
In an abstract class, the slot for the abstract method is created in the vtable, but the method's address is initially set to null. When a derived class overrides the method, the actual implementation is stored in the corresponding vtable entry.
How the CLR Handles Interfaces
When calling a method through an interface, the CLR also uses the callvirt
instruction, but there is a crucial difference. Instead of the vtable, the method resolution occurs through an interface table (ITable).
IL_0013: ldloc.1 // class2
IL_0014: callvirt instance void AbstractVsInterface.ConcreteClass2::Print()
IL_0019: nop
The CLR creates a separate ITable for each interface a class implements. This table maps interface methods to their corresponding implementations in the derived class.
Method Implementation in Derived Classes
In the derived class of an abstract class, the method is marked as virtual, and its implementation is stored in the vtable:
.method public hidebysig virtual instance void
Print() cil managed
{
.maxstack 8
// [5 37 - 5 82]
IL_0000: ldstr "This is concrete class 1"
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: nop
IL_000b: ret
} // end of method ConcreteClass1::Print
For interfaces, the derived class uses the newslot
keyword. This indicates that a new entry is created in the ITable for this implementation:
.method public final hidebysig virtual newslot instance void
Print() cil managed
{
.maxstack 8
// [10 28 - 10 73]
IL_0000: ldstr "This is concrete class 2"
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: nop
IL_000b: ret
} // end of method ConcreteClass2::Print
Object Inheritance and Method Resolution
Another key difference is how the CLR handles inheritance. Abstract classes are directly inherited from the System.Object
, while interfaces do not. Here is an excerpt from the metadata of an abstract class:
.class public abstract auto ansi beforefieldinit
AbstractVsInterface.SomeAbstractClass
extends [System.Runtime]System.Object
{
.method public hidebysig virtual newslot abstract instance void
Print() cil managed
{
// Can't find a body
} // end of method SomeAbstractClass::Print
.method family hidebysig specialname rtspecialname instance void
.ctor() cil managed
In contrast, the metadata for an interface shows that it does not extend the System.Object
directly:
.class interface public abstract auto ansi beforefieldinit
AbstractVsInterface.ISomeInterface
{
.method public hidebysig virtual newslot abstract instance void
Print() cil managed
{
// Can't find a body
} // end of method ISomeInterface::Print
} // end of class AbstractVsInterface.ISomeInterface
Performance Considerations
From a performance perspective, abstract classes are typically faster than interfaces due to the way method resolution is handled:
- Abstract Classes: Since there is only one vtable, method lookup is more efficient.
- Interfaces: The CLR needs to:
- Identify the correct interface.
- Locate the appropriate ITable.
- Perform the method lookup.
When a class implements multiple interfaces, the CLR must search through multiple interface tables, making the resolution process slower compared to abstract classes.
Summary
- Abstract Classes use a vtable for method dispatch, while Interfaces use an ITable.
- Abstract methods must be overridden, while interface methods must be implemented.
- Abstract classes inherit from
System.Object
, while interfaces do not. - Performance: Abstract classes are generally faster due to more straightforward method lookup.
Understanding these low-level differences can help you make better design decisions and optimize your applications more effectively.
I hope this article was helpful—happy coding!
Top comments (0)