Skip to main content

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

  1. Cannot be instantiated directly
  2. May contain abstract methods (methods without implementation)
  3. May contain concrete methods (methods with implementation)
  4. May contain properties, fields, constructors, etc.
  5. 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#:

csharp
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 and CalculatePerimeter)

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:

csharp
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:

csharp
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:

csharp
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:

FeatureAbstract ClassInterface
InstantiationCannot be instantiatedCannot be instantiated
MethodsCan have implementationsOnly default implementations in C# 8+
PropertiesCan have implementationsOnly default implementations in C# 8+
FieldsCan contain fieldsCannot contain fields
ConstructorsCan have constructorsNo constructors
InheritanceSingle inheritance onlyMultiple inheritance possible
Access ModifiersCan have various modifiersAll 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:

  1. When you want to share code among related classes: Abstract classes can provide common functionality for derived classes.

  2. Base functionality with specific behavior: When you want to provide a base implementation while forcing derived classes to implement specific methods.

  3. Hierarchical relationships: When you have a natural hierarchy of classes with a common base.

  4. 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:

csharp
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:

csharp
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:

  1. Define a common base with shared code
  2. Enforce implementation of specific methods in derived classes
  3. Create a type hierarchy that supports polymorphism
  4. 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

  1. Create an abstract BankAccount class with abstract methods Deposit, Withdraw, and a property Balance. Then implement CheckingAccount and SavingsAccount classes.

  2. Design an abstract Employee class with properties for name, ID, and salary. Include an abstract method CalculatePay(). Implement classes for HourlyEmployee and SalariedEmployee.

  3. Create an abstract Vehicle class hierarchy with different types of vehicles (Car, Motorcycle, Truck) that inherit from it. Include abstract methods for StartEngine() and StopEngine().

  4. Extend the game entity example by adding more entity types like NPC (non-player character) and Item.

Additional Resources

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! :)