Abstract Classes
Summary
An abstract class is a class that can never be instantiated directly — it
exists only to be inherited from, providing shared implementation to its
subclasses while explicitly requiring each of them to supply certain
methods (abstract methods) it deliberately leaves unimplemented. It sits
between a concrete base class (fully usable on its own) and an interface
(no implementation at all), giving both shared code and an enforced
contract at the same time.
Why This Matters
A concrete base Enemy class with a default Attack() implementation
doesn't stop someone from writing new Enemy() directly and getting a
generic, purposeless enemy that was never meant to exist on its own — every
real enemy was supposed to be a specific subtype. Meanwhile, an interface
alone can require every implementer to define Attack(), but can't share
any actual code between them, forcing MoveTowardPlayer() or
PlayHitReaction() to be duplicated in every single enemy type. An
abstract class solves both problems: new Enemy() becomes a compile error,
and shared logic lives in one place while each subclass is still forced to
supply its own Attack().
Mental Model
An abstract class is an incomplete blueprint stamped "not for construction as-is." A blueprint for "a house" might fully specify the foundation, plumbing, and wiring, while leaving the roof style as a required decision — you cannot build directly from this blueprint; you must first produce a specific variant (a "Colonial" or "Ranch" blueprint) that fills in the missing roof design. The abstract class is the same: it supplies everything that's common and settled, and marks what's still an open decision as something every concrete subclass must resolve.
Core Explanation
abstracton a class prevents direct instantiation.new Enemy()is a compile error ifEnemyis declaredabstract class Enemy, even though the class can still have fields, concrete methods, and constructors — only the directnewis disallowed.abstractmethods have no body and must be overridden.public abstract void Attack();declares the method's signature only; every non-abstract subclass is required to provide anoverridefor it, or the subclass fails to compile.- Concrete (non-abstract) members are inherited normally. An abstract class can still define fully-implemented methods, fields, and constructors that every subclass gets for free — abstract classes are not "pure contracts" the way interfaces are; they mix shared implementation with enforced gaps.
- A subclass that doesn't implement every abstract member must itself be abstract. There's no way to "partially" satisfy an abstract base — either every abstract member is overridden, or the subclass explicitly defers the remaining ones by also being abstract.
Syntax / Theory
C# (Unity)
public abstract class Enemy : MonoBehaviour
{
[SerializeField] protected int maxHealth = 100;
protected int currentHealth;
protected virtual void Awake() => currentHealth = maxHealth; // Shared, concrete.
public abstract void Attack(); // No body — every subclass must supply one.
public void TakeDamage(int amount) // Shared, concrete — every subclass gets this free.
{
currentHealth -= amount;
if (currentHealth <= 0) Die();
}
protected virtual void Die() => Destroy(gameObject);
}
public class RangedEnemy : Enemy
{
public override void Attack() => FireProjectile(); // Required — compile error without it.
}
// new Enemy(); // Compile error: cannot create an instance of the abstract class 'Enemy'.
Enemy e = new RangedEnemy(); // Fine — a concrete subclass satisfies every abstract member.
C++ (Unreal — pure virtual functions)
class AEnemy : public ACharacter
{
public:
void TakeDamage(int32 Amount); // Shared, concrete.
virtual void Attack() PURE_VIRTUAL(AEnemy::Attack, ); // "= 0" equivalent — abstract.
};
C++ has no abstract keyword on the class itself — a class becomes
abstract automatically the moment it declares any pure virtual function
(virtual void Attack() = 0;), and the compiler then refuses to
instantiate it directly, exactly matching C#'s behavior.
Unreal Blueprint
A C++ class marked UCLASS(Abstract) cannot be placed directly in a level
or spawned — only its Blueprint subclasses can — mirroring an abstract
class's "must be subclassed to be used" rule at the editor level.
Practical Example
A save-data exporter defines shared export scaffolding while requiring each concrete format to supply its own serialization step.
public abstract class DataExporter
{
public void Export(string path) // Shared workflow, same for every format.
{
string content = Serialize();
File.WriteAllText(path, content);
Debug.Log($"Exported to {path}");
}
protected abstract string Serialize(); // Each format decides this differently.
}
public class JsonExporter : DataExporter
{
protected override string Serialize() => JsonUtility.ToJson(data);
}
public class CsvExporter : DataExporter
{
protected override string Serialize() => BuildCsvString(data);
}
Export() — the file-writing and logging — is written once; only the
part that genuinely differs between formats (Serialize()) is left for
each subclass to fill in.
Common Mistakes
- Reaching for an interface when shared implementation is actually needed too. An interface alone cannot hold field state or concrete method bodies — if multiple implementers need identical logic, not just an identical contract, an abstract class (or an interface plus composition) fits better.
- Making a class abstract with no abstract members at all. If every
method already has a default implementation, marking the class
abstractonly to prevent direct instantiation is usually a sign a private constructor plus a factory method, or simply a concrete base class, was the better fit. - Forgetting that a subclass failing to implement one abstract member must itself be declared abstract. This is enforced by the compiler, but the error message ("does not implement inherited abstract member") can be confusing the first time it's encountered.
- Overusing abstract classes for things that never actually vary. An abstract method that every current and realistically-foreseeable subclass implements identically isn't earning its abstraction — it adds a required override with no actual behavioral difference to justify it.
Game Development Example
An ability system shares cooldown and resource-cost handling across every ability, while requiring each ability to define what actually happens when cast.
public abstract class Ability
{
public float Cooldown { get; protected set; }
private float lastCastTime = float.NegativeInfinity;
public bool CanCast(float currentTime) =>
currentTime - lastCastTime >= Cooldown;
public void TryCast(float currentTime)
{
if (!CanCast(currentTime)) return;
Execute();
lastCastTime = currentTime;
}
protected abstract void Execute(); // Every ability defines its own effect.
}
public class Fireball : Ability
{
public Fireball() => Cooldown = 3f;
protected override void Execute() => SpawnProjectile("Fireball");
}
Adding a new ability means writing only Execute() — the cooldown-gating
logic in TryCast/CanCast is written once and shared by every ability
that will ever exist in the game.
Best Practices
- Make a class abstract only when it has at least one member that genuinely differs per subclass and cannot be given a sensible default.
- Put every piece of logic that's identical across all subclasses as a concrete member on the abstract class — that's the entire point of choosing an abstract class over an interface.
- Keep abstract method contracts small and focused — a subclass forced to implement five unrelated abstract methods to satisfy one base class is a sign the base class is doing too much.
- Consider whether an interface (see [[CON-0011]]), possibly combined with composition, would satisfy the same need without forcing a single inheritance chain.
Exercises
- Write an abstract
Collectibleclass with a concreteOnPickedUp()that plays a sound and destroys the object, and an abstractApplyEffect()that subclasses likeHealthPickupandCoinPickupimplement differently. - Take the
DataExporterexample and add anXmlExportersubclass, confirming thatExport()requires no changes at all to support the new format. - You're designing a
UIWidgetbase for a UI system with 30+ widget types, most of which share layout logic but differ significantly in rendering. Decide between an abstract base class and an interface plus a separate shared layout helper class, and justify the trade-off. - Research the difference between an abstract class and a C#
sealedclass, and explain why a class can never be bothabstractandsealedat the same time.
Related Concepts
- Inheritance — abstract classes are a specialized form of inheritance where the base class deliberately leaves some behavior unimplemented; understanding ordinary inheritance is the necessary foundation.
- Interfaces — the closest alternative to an abstract class when no shared implementation is needed, only a shared contract; choosing between them is a recurring design decision.
- Method Overriding — abstract methods are satisfied through the exact
same
overridemechanism as overriding avirtualmethod, just with a base that provides no default to fall back on.
Further Reading
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Microsoft C# documentation — Abstract and Sealed Classes: https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members
- Unreal Engine C++ documentation — Abstract Classes and Pure Virtual Functions: https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-classes-in-unreal-engine