C# Abstract Classes
An abstract class is a class that can define shared code while also requiring derived classes to fill in missing behavior. You use one when a base type represents a real concept, but is incomplete on its own, such as Shape, Employee, or DocumentExporter. Abstract classes matter because they let you combine inheritance, polymorphism, shared state, and enforced rules in one design.
Overview: How Abstract Classes Work
In C#, a class marked with abstract cannot be instantiated directly. You cannot write new Shape() if Shape is abstract, because the type may contain members with no implementation. Instead, another class derives from it and supplies the missing pieces. A concrete derived class is one that is no longer abstract and can be created with new.
An abstract class can contain ordinary fields, constructors, properties, and methods. It can also contain abstract members, such as methods or properties that declare a signature but have no body. These abstract members are promises: every non-abstract derived class must override them. This is stronger than a virtual method. A virtual method says, “Here is default behavior; override it if needed.” An abstract method says, “This behavior is required, but the base class cannot define it.”
Abstract classes are often used as polymorphic base types. A variable declared as an abstract base class can hold any concrete derived object. For example, a Shape variable can refer to a Circle or Rectangle. When code calls an abstract or virtual member, the CLR dispatches the call to the override on the object’s actual runtime type. The object is not converted into the abstract type; the reference simply views it through the base contract.
Constructors in abstract classes are important. Even though you cannot instantiate the abstract class directly, its constructor still runs as part of constructing a derived object. This lets the base class initialize shared state, such as a name, identifier, or common validation rule. Derived constructors call the base constructor with : base(...), either explicitly or implicitly if a parameterless constructor exists.
Use an abstract class when derived types are strongly related and benefit from shared implementation or protected state. Use an interface when unrelated types merely share a capability. A class can inherit from only one base class, abstract or not, but it can implement many interfaces. That limitation means an abstract base class should represent an important inheritance relationship, not just a convenient bundle of methods.
Syntax
abstract class BaseType
{
protected BaseType(string name)
{
Name = name;
}
public string Name { get; }
public abstract decimal Calculate();
public virtual string Describe()
{
return Name;
}
}
class DerivedType : BaseType
{
public DerivedType(string name) : base(name)
{
}
public override decimal Calculate()
{
return 10m;
}
}
| Part | Meaning |
|---|---|
abstract class |
Declares a class that cannot be instantiated directly. |
abstract member |
Declares a required member with no body in the abstract class. |
override |
Supplies the implementation for an inherited abstract or virtual member. |
protected |
Makes a member available to the base class and derived classes, but not general callers. |
: base(...) |
Calls a constructor on the abstract base class while creating a derived object. |
virtual |
Provides default behavior that derived classes may replace. |
Examples
Example 1: A Simple Abstract Shape
using System;
using System.Globalization;
abstract class Shape
{
public Shape(string name)
{
Name = name;
}
public string Name { get; }
public abstract double Area();
}
class Rectangle : Shape
{
private readonly double width;
private readonly double height;
public Rectangle(double width, double height) : base("Rectangle")
{
this.width = width;
this.height = height;
}
public override double Area()
{
return width * height;
}
}
class Program
{
static void Main()
{
Shape shape = new Rectangle(4, 3);
string area = shape.Area().ToString("0.0", CultureInfo.InvariantCulture);
Console.WriteLine($"{shape.Name} area: {area}");
}
}
Output:
Rectangle area: 12.0
Shape stores the shared Name property, but it cannot calculate an area without knowing the specific shape. Rectangle provides the required Area override. The variable is typed as Shape, yet the runtime calls Rectangle.Area.
Example 2: Shared Constructor and Protected Helper
using System;
using System.Globalization;
abstract class Employee
{
public Employee(string name, decimal basePay)
{
Name = name;
BasePay = basePay;
}
public string Name { get; }
protected decimal BasePay { get; }
public abstract decimal CalculateWeeklyPay();
protected string FormatMoney(decimal amount)
{
return amount.ToString("0.00", CultureInfo.InvariantCulture);
}
public string BuildPayLine()
{
return $"{Name}: ${FormatMoney(CalculateWeeklyPay())}";
}
}
class SalariedEmployee : Employee
{
public SalariedEmployee(string name, decimal weeklySalary) : base(name, weeklySalary)
{
}
public override decimal CalculateWeeklyPay()
{
return BasePay;
}
}
class HourlyEmployee : Employee
{
private readonly decimal hoursWorked;
public HourlyEmployee(string name, decimal hourlyRate, decimal hoursWorked) : base(name, hourlyRate)
{
this.hoursWorked = hoursWorked;
}
public override decimal CalculateWeeklyPay()
{
return BasePay * hoursWorked;
}
}
class Program
{
static void Main()
{
Employee[] employees =
{
new SalariedEmployee("Mina", 900m),
new HourlyEmployee("Omar", 25m, 32m)
};
foreach (Employee employee in employees)
{
Console.WriteLine(employee.BuildPayLine());
}
}
}
Output:
Mina: $900.00
Omar: $800.00
The base class owns the common pay-line formatting and the shared state. Each employee type owns only its pay calculation rule. Notice that BuildPayLine is not abstract; it calls the abstract CalculateWeeklyPay, so the common method still benefits from polymorphism.
Example 3: Abstract and Virtual Members Together
using System;
abstract class MessageSender
{
public abstract string Channel { get; }
public void Send(string recipient, string message)
{
Console.WriteLine($"[{Channel}] To {recipient}: {Format(message)}");
}
protected virtual string Format(string message)
{
return message;
}
}
class EmailSender : MessageSender
{
public override string Channel => "Email";
}
class SmsSender : MessageSender
{
public override string Channel => "SMS";
protected override string Format(string message)
{
return message.Length <= 12 ? message : message.Substring(0, 12);
}
}
class Program
{
static void Main()
{
MessageSender[] senders = { new EmailSender(), new SmsSender() };
foreach (MessageSender sender in senders)
{
sender.Send("Alex", "Deployment ready");
}
}
}
Output:
[Email] To Alex: Deployment ready
[SMS] To Alex: Deployment
Channel is abstract because every sender must name its channel. Format is virtual because the base behavior is acceptable for most senders, but SMS needs a shorter message. The public Send method coordinates the algorithm while controlled extension points customize parts of it.
How It Works Step by Step
- The compiler sees
abstract classand marks the type as incomplete in metadata. - If the class declares an abstract member, the compiler requires a semicolon instead of a method body.
- Every concrete derived class must implement inherited abstract members with matching
overridemembers. - When code creates a derived object, the CLR allocates the concrete type, then runs the base constructor before the derived constructor body.
- A base-class reference can point at the concrete object because the derived class is also a kind of the abstract base class.
- Calls to abstract and virtual members use runtime dispatch, so the most specific override runs.
- Non-virtual members on the abstract base class are called normally, but they can internally call abstract or virtual members and receive derived behavior.
This is why abstract classes can express a template: the base class can contain a stable workflow, while derived classes fill in specific steps. The design is powerful, but it also creates a contract that derived classes must respect.
Common Mistakes
Mistake 1: Trying to Instantiate an Abstract Class
abstract class Report
{
public abstract string Title();
}
class Program
{
static void Main()
{
Report report = new Report();
}
}
This does not compile because Report is abstract. Create a concrete derived class that implements the required members, then instantiate that class.
using System;
abstract class Report
{
public abstract string Title();
}
class SalesReport : Report
{
public override string Title()
{
return "Sales Report";
}
}
class Program
{
static void Main()
{
Report report = new SalesReport();
Console.WriteLine(report.Title());
}
}
Output:
Sales Report
Mistake 2: Forgetting to Override Every Abstract Member
abstract class Exporter
{
public abstract string FileExtension { get; }
public abstract string Export(string text);
}
class JsonExporter : Exporter
{
public override string FileExtension => ".json";
}
JsonExporter does not compile because it implements FileExtension but forgets Export. Either implement all inherited abstract members or mark JsonExporter as abstract too.
using System;
abstract class Exporter
{
public abstract string FileExtension { get; }
public abstract string Export(string text);
}
class JsonExporter : Exporter
{
public override string FileExtension => ".json";
public override string Export(string text)
{
return "{\"value\":\"" + text + "\"}";
}
}
class Program
{
static void Main()
{
Exporter exporter = new JsonExporter();
Console.WriteLine(exporter.FileExtension);
Console.WriteLine(exporter.Export("ok"));
}
}
Output:
.json
{"value":"ok"}
Mistake 3: Choosing an Abstract Class for an Unrelated Capability
If EmailSender, FileLogger, and ConsoleLogger only share a Write capability, an interface is usually better than an abstract class. An abstract class spends the single inheritance slot, so use it when there is a real base identity or shared implementation.
Best Practices
- Use abstract classes for closely related types that share state, constructors, or common method implementations.
- Keep abstract members small and purposeful; every concrete derived class must implement them.
- Prefer
protectedmembers for helpers meant only for derived classes, and keep the public API clean. - Avoid calling abstract or virtual members from constructors because derived fields may not be initialized yet.
- Use
virtualwhen a default implementation exists, andabstractwhen no correct default exists. - Design the base class contract carefully before publishing it, because changing abstract members breaks all concrete derived classes.
- Do not use abstract classes just to avoid code duplication between unrelated types; prefer composition or interfaces.
- Consider sealing concrete derived classes when they are not designed for further inheritance.
Practice Exercises
- Create an abstract
BankAccountclass with a sharedBalanceproperty and an abstractWithdrawmethod. Implement checking and savings accounts with different rules. - Create an abstract
GameCharacterclass with a constructor forName, an abstractAttackmethod, and a concreteIntroducemethod. - Build an abstract
DocumentConverterwith an abstractTargetExtensionproperty and a virtualNormalizeTextmethod. Override the virtual method in one derived converter.
Summary
- An abstract class cannot be instantiated directly and may contain incomplete abstract members.
- Concrete derived classes must override all inherited abstract members.
- Abstract classes can still have constructors, fields, properties, ordinary methods, and virtual methods.
- Base constructors run when derived objects are created, so abstract classes can initialize shared state.
- Abstract base references enable polymorphism: the variable type is general, but runtime dispatch calls derived overrides.
- Use abstract classes for real inheritance relationships with shared implementation; use interfaces for shared capabilities across unrelated types.
