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:
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; }
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:
var s = new Shape(); abstract class Shape { public abstract double Area(); }
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:
abstract class Shape { public abstract double Area(); public abstract double Perimeter(); } class Square(double side) : Shape { public override double Area() => side * side; }
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:
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); }
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:
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 ---"; }
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 class | Interface | |
|---|---|---|
| How many can a class use? | One base class | Any number |
| Fields and constructors | Yes | No instance fields or constructors |
| Shared implementation | Yes, naturally | Limited (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.
25% off $80.00 -> $60.00 $15.00 off $80.00 -> $65.00 0% off $80.00 -> $80.00