Abstract Classes

Some base classes describe an idea that's too general to exist on its own. There's no such thing as a plain "shape" — only circles, squares and so on. An abstract class captures that: it can't be instantiated, and it can declare abstract members that every derived class is forced to implement.

Abstract classes and methods

An abstract method has a signature but no body. Any non-abstract class that derives from the abstract class must override it:

C#
Shape[] shapes = [new Circle(2), new Rectangle(3, 4)];
foreach (Shape s in shapes)
{
    s.Print();
}

abstract class Shape
{
    public abstract string Name { get; }
    public abstract double Area();

    public void Print() => Console.WriteLine($"{Name} with area {Area():F2}");
}

class Circle(double r) : Shape
{
    public override string Name => "Circle";
    public override double Area() => Math.PI * r * r;
}

class Rectangle(double w, double h) : Shape
{
    public override string Name => "Rectangle";
    public override double Area() => w * h;
}
Output
Circle with area 12.57
Rectangle with area 12.00

Compare this with the previous lesson, where Shape.Area() returned a meaningless 0. Making it abstract says what's actually true: every shape has an area, but only concrete shapes know how to compute it. An abstract class can still contain normal members, like Print, which all derived classes share.

The compiler enforces the rules

You can't create an instance of an abstract class:

C#
var s = new Shape();

abstract class Shape
{
    public abstract double Area();
}
Build error
Program.cs(1,9): error CS0144: Cannot create an instance of the abstract type or interface 'Shape'

And forgetting to implement an abstract member is an error, not a silent bug:

C#
abstract class Shape
{
    public abstract double Area();
    public abstract double Perimeter();
}

class Square(double side) : Shape
{
    public override double Area() => side * side;
}
Build error
Program.cs(7,7): error CS0534: 'Square' does not implement inherited abstract member 'Shape.Perimeter()'

Abstract classes with constructors and state

An abstract class can have fields, properties and constructors. Derived classes call the constructor with base(...) as usual:

C#
var accounts = new List<Account>
{
    new CheckingAccount("Kim", 200m),
    new SavingsAccount("Lou", 5000m),
};

foreach (Account a in accounts)
{
    a.EndOfMonth();
    Console.WriteLine($"{a.Owner}: {a.Balance:C} ({a.Kind})");
}

abstract class Account(string owner, decimal balance)
{
    public string Owner { get; } = owner;
    public decimal Balance { get; protected set; } = balance;
    public abstract string Kind { get; }
    public abstract void EndOfMonth();
}

class CheckingAccount(string owner, decimal balance) : Account(owner, balance)
{
    public override string Kind => "checking";
    public override void EndOfMonth() => Balance -= 5m;   // monthly fee
}

class SavingsAccount(string owner, decimal balance) : Account(owner, balance)
{
    public override string Kind => "savings";
    public override void EndOfMonth() => Balance += Math.Round(Balance * 0.004m, 2);
}
Output
Kim: $195.00 (checking)
Lou: $5,020.00 (savings)

The template method pattern

A classic use of abstract classes: the base class defines the overall steps of an algorithm in a regular method, and derived classes fill in specific steps. The order of the steps is fixed in one place:

C#
ReportGenerator[] generators = [new CsvReport(), new TextReport()];
string[][] rows = [["Apples", "12"], ["Pears", "7"]];

foreach (var g in generators)
{
    Console.WriteLine(g.Generate(rows));
}

abstract class ReportGenerator
{
    public string Generate(string[][] rows)
    {
        var lines = new List<string> { Header() };
        foreach (string[] row in rows)
            lines.Add(FormatRow(row));
        lines.Add(Footer(rows.Length));
        return string.Join("\n", lines);
    }

    protected abstract string Header();
    protected abstract string FormatRow(string[] row);
    protected virtual string Footer(int count) => $"({count} rows)";
}

class CsvReport : ReportGenerator
{
    protected override string Header() => "item,quantity";
    protected override string FormatRow(string[] row) => string.Join(",", row);
}

class TextReport : ReportGenerator
{
    protected override string Header() => "ITEM       QTY";
    protected override string FormatRow(string[] row) => $"{row[0],-10} {row[1],3}";
    protected override string Footer(int count) => $"--- {count} items ---";
}
Output
item,quantity
Apples,12
Pears,7
(2 rows)
ITEM       QTY
Apples      12
Pears        7
--- 2 items ---

Footer is virtual rather than abstract: derived classes may override it, but don't have to. The steps are protected because they're part of how the class works, not something outside code should call.

Abstract class or interface?

Interfaces (next lesson) also define members that classes must implement. The main differences:

Abstract classInterface
How many can a class use?One base classAny number
Fields and constructorsYesNo instance fields or constructors
Shared implementationYes, naturallyLimited (default methods)
Relationship"is a" — a family of closely related classes"can do" — a capability unrelated classes share

Use an abstract class when related classes share state and code. Use an interface to describe a capability, especially across unrelated types.

Try it yourself

Create an abstract Discount with an abstract Apply method, and two concrete discounts.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
25% off      $80.00 -> $60.00
$15.00 off   $80.00 -> $65.00
0% off       $80.00 -> $80.00