C# Abstract Classes
Introduction
In object-oriented programming, abstract classes are a fundamental concept that helps in creating hierarchical class structures. An abstract class in C# is a special type of class that cannot be instantiated directly, but can only be inherited from. Think of abstract classes as "blueprints" for other classes, providing a foundation of functionality while allowing derived classes to implement or override specific behaviors.
In this tutorial, you'll learn:
- What abstract classes are in C#
- How to define and use abstract classes
- Abstract methods and properties
- Differences between abstract classes and interfaces
- When to use abstract classes in real-world applications
Understanding Abstract Classes
An abstract class is declared using the abstract
keyword. It may contain both abstract and non-abstract (concrete) members. The purpose of an abstract class is to provide a common base class definition for subclasses, ensuring that they implement certain methods or properties.
Key Characteristics
- Cannot be instantiated directly
- May contain abstract methods (methods without implementation)
- May contain concrete methods (methods with implementation)
- May contain properties, fields, constructors, etc.
- Subclasses must implement all abstract members unless the subclass is also abstract
Creating an Abstract Class
Here's how to define a simple abstract class in C#:
public abstract class Shape
{
// Properties
public string Color { get; set; }
// Constructor
public Shape(string color)
{
Color = color;
}
// Concrete method
public void DisplayColor()
{
Console.WriteLine($"This shape is {Color}");
}
// Abstract method (no implementation)
public abstract double CalculateArea();
// Abstract method (no implementation)
public abstract double CalculatePerimeter();
}
In this example, Shape
is an abstract class with:
- A concrete property (
Color
) - A concrete method (
DisplayColor
) - Two abstract methods (
CalculateArea
andCalculatePerimeter
)
Implementing an Abstract Class
To use an abstract class, we need to create a derived class that inherits from it and implements all of its abstract members:
public class Circle : Shape
{
public double Radius { get; set; }
// Constructor
public Circle(string color, double radius) : base(color)
{
Radius = radius;
}
// Implementing abstract methods
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
}
public class Rectangle : Shape
{
public double Length { get; set; }
public double Width { get; set; }
// Constructor
public Rectangle(string color, double length, double width) : base(color)
{
Length = length;
Width = width;
}
// Implementing abstract methods
public override double CalculateArea()
{
return Length * Width;
}
public override double CalculatePerimeter()
{
return 2 * (Length + Width);
}
}
Using Abstract Classes
Now, let's see how to use these classes in a program:
class Program
{
static void Main(string[] args)
{
// Cannot create an instance of an abstract class
// Shape shape = new Shape("Red"); // This would cause a compilation error
// Create instances of derived classes
Circle circle = new Circle("Blue", 5);
Rectangle rectangle = new Rectangle("Green", 4, 6);
// Accessing properties and methods
circle.DisplayColor(); // From base class
Console.WriteLine($"Circle area: {circle.CalculateArea()}");
Console.WriteLine($"Circle perimeter: {circle.CalculatePerimeter()}");
rectangle.DisplayColor(); // From base class
Console.WriteLine($"Rectangle area: {rectangle.CalculateArea()}");
Console.WriteLine($"Rectangle perimeter: {rectangle.CalculatePerimeter()}");
// Using polymorphism with an array of abstract type
Shape[] shapes = { circle, rectangle };
foreach (Shape shape in shapes)
{
shape.DisplayColor();
Console.WriteLine($"Area: {shape.CalculateArea()}");
Console.WriteLine($"Perimeter: {shape.CalculatePerimeter()}");
Console.WriteLine();
}
}
}
Output:
This shape is Blue
Circle area: 78.53981633974483
Circle perimeter: 31.41592653589793
This shape is Green
Rectangle area: 24
Rectangle perimeter: 20
This shape is Blue
Area: 78.53981633974483
Perimeter: 31.41592653589793
This shape is Green
Area: 24
Perimeter: 20
Abstract Properties
Just like methods, C# also allows you to create abstract properties in abstract classes:
public abstract class Document
{
// Abstract property
public abstract string Title { get; set; }
// Abstract read-only property
public abstract int PageCount { get; }
// Concrete method
public void Display()
{
Console.WriteLine($"Document: {Title}, Pages: {PageCount}");
}
}
public class Book : Document
{
private string _title;
private int _pageCount;
// Constructor
public Book(string title, int pageCount)
{
_title = title;
_pageCount = pageCount;
}
// Implementing abstract properties
public override string Title
{
get { return _title; }
set { _title = value; }
}
public override int PageCount
{
get { return _pageCount; }
}
}
Abstract Classes vs. Interfaces
While abstract classes and interfaces may seem similar, they serve different purposes:
Feature | Abstract Class | Interface |
---|---|---|
Instantiation | Cannot be instantiated | Cannot be instantiated |
Methods | Can have implementations | Only default implementations in C# 8+ |
Properties | Can have implementations | Only default implementations in C# 8+ |
Fields | Can contain fields | Cannot contain fields |
Constructors | Can have constructors | No constructors |
Inheritance | Single inheritance only | Multiple inheritance possible |
Access Modifiers | Can have various modifiers | All members are public by default |
Purpose | "Is-a" relationship | "Can-do" relationship |
When to Use Abstract Classes
Abstract classes are ideal in the following scenarios:
-
When you want to share code among related classes: Abstract classes can provide common functionality for derived classes.
-
Base functionality with specific behavior: When you want to provide a base implementation while forcing derived classes to implement specific methods.
-
Hierarchical relationships: When you have a natural hierarchy of classes with a common base.
-
When you need to maintain state: Abstract classes can contain fields to maintain state information.
Real-World Example: Game Development
Let's implement a simple game entity system using abstract classes:
public abstract class GameEntity
{
public string Name { get; protected set; }
public int PositionX { get; protected set; }
public int PositionY { get; protected set; }
public bool IsActive { get; protected set; }
public GameEntity(string name, int x, int y)
{
Name = name;
PositionX = x;
PositionY = y;
IsActive = true;
}
// Concrete method
public void Move(int deltaX, int deltaY)
{
PositionX += deltaX;
PositionY += deltaY;
Console.WriteLine($"{Name} moved to position ({PositionX}, {PositionY})");
}
// Abstract methods that must be implemented by derived classes
public abstract void Render();
public abstract void Update();
// Virtual method that can be overridden
public virtual void TakeDamage(int amount)
{
Console.WriteLine($"{Name} took {amount} damage");
}
}
public class Player : GameEntity
{
public int Health { get; private set; }
public Player(string name, int x, int y) : base(name, x, y)
{
Health = 100;
}
public override void Render()
{
Console.WriteLine($"Rendering player {Name} at ({PositionX}, {PositionY})");
}
public override void Update()
{
Console.WriteLine($"Updating player {Name}");
}
public override void TakeDamage(int amount)
{
base.TakeDamage(amount);
Health -= amount;
Console.WriteLine($"Player {Name}'s health is now {Health}");
if (Health <= 0)
{
IsActive = false;
Console.WriteLine($"Player {Name} has been defeated!");
}
}
}
public class Enemy : GameEntity
{
public int Damage { get; private set; }
public Enemy(string name, int x, int y, int damage) : base(name, x, y)
{
Damage = damage;
}
public override void Render()
{
Console.WriteLine($"Rendering enemy {Name} at ({PositionX}, {PositionY})");
}
public override void Update()
{
Console.WriteLine($"Updating enemy {Name}");
}
public void Attack(Player player)
{
Console.WriteLine($"Enemy {Name} attacks player {player.Name}");
player.TakeDamage(Damage);
}
}
Here's how we might use this game entity system:
class Program
{
static void Main(string[] args)
{
// Create game entities
Player hero = new Player("Hero", 10, 10);
Enemy goblin = new Enemy("Goblin", 5, 5, 10);
Enemy troll = new Enemy("Troll", 15, 15, 20);
// Create a list of entities
List<GameEntity> entities = new List<GameEntity> { hero, goblin, troll };
// Game loop
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"\n--- Game Tick {i+1} ---");
// Update and render all entities
foreach (var entity in entities)
{
if (entity.IsActive)
{
entity.Update();
entity.Render();
}
}
// Move the player
hero.Move(1, 1);
// Enemies attack if they're close enough
if (goblin.IsActive)
{
goblin.Attack(hero);
}
if (troll.IsActive && i == 1) // Troll attacks on second iteration
{
troll.Attack(hero);
}
}
}
}
Output:
--- Game Tick 1 ---
Updating player Hero
Rendering player Hero at (10, 10)
Updating enemy Goblin
Rendering enemy Goblin at (5, 5)
Updating enemy Troll
Rendering enemy Troll at (15, 15)
Hero moved to position (11, 11)
Enemy Goblin attacks player Hero
Hero took 10 damage
Player Hero's health is now 90
--- Game Tick 2 ---
Updating player Hero
Rendering player Hero at (11, 11)
Updating enemy Goblin
Rendering enemy Goblin at (5, 5)
Updating enemy Troll
Rendering enemy Troll at (15, 15)
Hero moved to position (12, 12)
Enemy Goblin attacks player Hero
Hero took 10 damage
Player Hero's health is now 80
Enemy Troll attacks player Hero
Hero took 20 damage
Player Hero's health is now 60
--- Game Tick 3 ---
Updating player Hero
Rendering player Hero at (12, 12)
Updating enemy Goblin
Rendering enemy Goblin at (5, 5)
Updating enemy Troll
Rendering enemy Troll at (15, 15)
Hero moved to position (13, 13)
Enemy Goblin attacks player Hero
Hero took 10 damage
Player Hero's health is now 50
Summary
Abstract classes in C# provide a powerful way to create class hierarchies with shared functionality while enforcing implementation of specific behaviors. They bridge the gap between concrete classes and interfaces, allowing you to:
- Define a common base with shared code
- Enforce implementation of specific methods in derived classes
- Create a type hierarchy that supports polymorphism
- Establish "is-a" relationships between objects
Remember that abstract classes cannot be instantiated directly, but serve as blueprints for derived classes. Use abstract classes when you want to share code among related classes while mandating certain functionality in subclasses.
Exercises
-
Create an abstract
BankAccount
class with abstract methodsDeposit
,Withdraw
, and a propertyBalance
. Then implementCheckingAccount
andSavingsAccount
classes. -
Design an abstract
Employee
class with properties for name, ID, and salary. Include an abstract methodCalculatePay()
. Implement classes forHourlyEmployee
andSalariedEmployee
. -
Create an abstract
Vehicle
class hierarchy with different types of vehicles (Car, Motorcycle, Truck) that inherit from it. Include abstract methods forStartEngine()
andStopEngine()
. -
Extend the game entity example by adding more entity types like
NPC
(non-player character) andItem
.
Additional Resources
- Microsoft Docs: Abstract and Sealed Classes
- C# Abstract Classes vs Interfaces
- Design Patterns that use Abstract Classes
Happy coding with abstract classes!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)