Skip to content

Abstract classes and interfaces

↑

Examples are independent C# snippets for modern .NET console projects unless noted otherwise. Class declarations and top-level code can live in Program.cs when top-level statements come first.

Abstraction describes what an object can do without exposing all implementation details. Abstract classes can share state; interfaces describe capabilities.

You cannot instantiate an abstract class directly. It can supply implementation and require derived classes to provide abstract members. C# still allows only one base class.

Example.cs
public abstract class Shape
{
public abstract double Area();
public string Label() => $"Area = {Area()}";
}
public sealed class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}

An interface declares a contract for implementing types. One class can implement several interfaces, enabling combinations of independent capabilities without multiple class inheritance. Modern C# interfaces can also contain default implementations, but start with simple contracts.

Example.cs
public interface IDamageable { void TakeDamage(int amount); }
public interface IResettable { void Reset(); }
public class Target : IDamageable, IResettable
{
private int health = 100;
public void TakeDamage(int amount) => health -= amount;
public void Reset() => health = 100;
}

Code against a small interface when multiple implementations or test substitutes are useful. Do not add an interface for every class automatically. A clear concrete class can be simpler.

The following snippet uses IDamageable and Target from the previous example.

Example.cs
public static void Hit(IDamageable target)
{
target.TakeDamage(10);
}
Hit(new Target());

Deep dive: decide between shared implementation and a contract

Section titled “Deep dive: decide between shared implementation and a contract”

An abstract class can provide both a rule and shared code

Section titled “An abstract class can provide both a rule and shared code”

You cannot instantiate an abstract class directly. It can declare abstract members that subclasses must implement (unless they are abstract themselves), plus ordinary fields, methods and constructors that descendants reuse. Choose it when different kinds of objects have a genuine common foundation and require shared implementation.

An interface says which operations an implementation offers, not which class it must inherit from. A class can implement many interfaces, which is why a damageable door, destructible crate and player can all implement IDamageable even though they belong to unrelated class hierarchies. Modern C# interfaces can contain default implementations and static members, but their core value is still a stable callable contract.

Program.cs
IDamageable target = new TrainingDummy();
target.TakeDamage(12);
public interface IDamageable
{
void TakeDamage(int amount);
}
public sealed class TrainingDummy : IDamageable
{
public int Health { get; private set; } = 50;
public void TakeDamage(int amount)
=> Health = Math.Max(0, Health - amount);
}

Code that accepts an IDamageable parameter does not need to know whether it received a player or a dummy. A test can supply a small fake implementation and verify that damage was requested. This is dependency inversion in a practical form: the caller relies on the behavior it needs instead of constructing or referencing a specific implementation. Prefer small interfaces that describe a coherent capability, not huge interfaces with methods most implementers cannot support.

Use an abstract class for shared base implementation and an interface for a role implemented by unrelated types.

Interface members are not automatically methods that all code can call on any object; the reference type and visibility still matter.

Define ISaveable with Save() and implement it in two unrelated classes.

Reveal a solution

interface ISaveable { void Save(); } followed by separate implementations.