Search⌘ K
AI Features

Abstract Classes

Explore the concept of abstract classes in C# to understand how they serve as blueprints in object-oriented programming. This lesson covers creating abstract classes, defining abstract members, and implementing constructors and methods in derived classes. Gain insight into how abstract classes enable code reuse and enforce design contracts by requiring derived classes to provide specific implementations. By the end, you'll comprehend why abstract classes cannot be instantiated directly and how to leverage them for clean, maintainable code.

C# includes abstract classes alongside concrete classes. An abstract class is a class that cannot be instantiated. It can hold shared functionality for its derived classes. This concept is a core pillar of abstraction in C#. By using abstract classes, we can hide the complex implementation details of a base concept and only expose the essential features that derived classes must have.

Abstract classes are useful when we need to define shared behavior without creating instances of the base concept. They serve as a blueprint, allowing us to share common logic while enforcing a design contract that requires derived classes to handle specific implementation details.

Creating an abstract class

Consider the Vehicle, Car, and Motorcycle classes. The Vehicle class holds members common to all vehicles. A vehicle is an abstract concept, while cars and motorcycles are concrete entities.

We mark the Vehicle class as abstract to prevent it from being instantiated directly.

C# 14.0
namespace AbstractClasses;
// Made our class abstract
public abstract class Vehicle
{
public string? Model { get; internal set; }
public decimal Price { get; internal set; }
public int NumberOfWheels { get; internal set; }
}
  • Line 1: We declare a file-scoped namespace to organize the code.

  • Line 4: We use the abstract keyword to define the class. This prevents the class from being instantiated directly.

  • Line 6: We make Model a nullable string (string?) because it might not be set immediately upon instantiation.

  • Lines 7–8: We define other properties that will be inherited by all derived classes.

Next, we implement a derived class that inherits from this abstract base.

C# 14.0
namespace AbstractClasses;
public class Motorcycle : Vehicle
{
public Motorcycle()
{
NumberOfWheels = 2;
}
}
  • Line 3: We inherit from Vehicle in the Motorcycle class.

  • Lines 5–8: The constructor sets NumberOfWheels to 2, a property inherited from the abstract base class.

We can define another derived class that extends the same abstract concept.

C# 14.0
namespace AbstractClasses;
public class Car : Vehicle
{
public int NumberOfSeatbelts { get; set; }
public Car()
{
NumberOfWheels = 4;
}
}
  • Line 3: We inherit from Vehicle in the Car class as well.

  • Line 5: We add a property specific to cars that does not exist on the base Vehicle class.

  • Lines 8: We set the inherited NumberOfWheels property to 4.

With our classes defined, let us see how we can and cannot instantiate them.

C# 14.0
using AbstractClasses;
// Can instantiate
Car car = new Car();
// Can instantiate
Motorcycle motorcycle = new Motorcycle();
// We can even do something like this
// because a Car object is created, not Vehicle
Vehicle anotherCar = new Car();
car.Model = "Lexus";
motorcycle.Model = "Yamaha";
anotherCar.Model = "Nissan";
// The following line would cause a compiler error:
Vehicle myVehicle = new Vehicle();
  • Lines 4 and 7: We successfully create instances of Car and Motorcycle.

  • Line 11: We create a variable of type Vehicle, but we assign it a Car instance, which is allowed because Car inherits from Vehicle.

  • Lines 13–15: We access the Model property, which is defined in the abstract Vehicle class but available on the instances.

  • Line 18: We would trigger a compiler error if we attempted new Vehicle() because Vehicle is abstract.

Constructors in abstract classes

Abstract classes can have custom constructors even though they cannot be instantiated directly. These custom constructors can set initial values for properties or perform other common tasks. In turn, these constructors can be called from derived-class constructors to avoid code repetition. Base-class constructors are called using the base keyword.

C# 14.0
namespace AbstractClasses;
public abstract class Vehicle
{
public string? Model { get; internal set; }
public decimal Price { get; internal set; }
public int NumberOfWheels { get; internal set; }
public Vehicle(string model, decimal price, int wheels)
{
Model = model;
Price = price;
NumberOfWheels = wheels;
}
}
  • Lines 9–14: We define a constructor in the abstract Vehicle class to initialize its properties.

Next, we must update our derived class to ensure it calls this new constructor correctly.

C# 14.0
namespace AbstractClasses;
public class Car : Vehicle
{
public int NumberOfSeatbelts { get; set; }
public Car(string model, decimal price)
: base(model, price, 4) // Calling the base class constructor
{
}
}
  • Line 6: We define a constructor for Car that accepts model and price.

  • Line 7: We pass the specific arguments (model, price) immediately to the base constructor. We hardcode the value 4 in the base call because all cars have four wheels in this context.

Finally, we can verify that the constructors work as expected by creating an instance in our main program.

C# 14.0
using AbstractClasses;
// We instantiate Car using the new constructor that accepts model and price
Car car = new Car("Lexus", 50000);
Console.WriteLine($"Model: {car.Model}");
Console.WriteLine($"Price: {car.Price}");
Console.WriteLine($"Wheels: {car.NumberOfWheels}");
  • Line 4: We create a new Car instance, passing “Lexus” and 50000 to the Car constructor. The Car constructor internally calls the base Vehicle constructor, passing these values along with the hardcoded wheel count of 4.

  • Lines 6–8: We print the values to verify that the properties were set correctly by the base constructor.

Abstract members

Abstract classes can have methods, properties, and other members just like concrete classes. Sometimes we must define a method signature without an implementation because there is no meaningful default behavior for derived classes. In these cases, we can mark a member as abstract and skip the implementation block entirely:

public abstract void Accelerate();

For our Vehicle class, we create an Accelerate() method without a body. Vehicles accelerate differently depending on the exact type of vehicle. It may be infeasible to provide a default implementation for this method. Therefore, we mark this method as abstract and delegate implementation responsibility to derived classes. A derived class must implement this method for the code to compile.

C# 14.0
namespace AbstractClasses;
public abstract class Vehicle
{
public string? Model { get; internal set; }
public decimal Price { get; internal set; }
public int NumberOfWheels { get; internal set; }
// Abstract method
public abstract void Accelerate();
}
  • Line 10: We mark the Accelerate method as abstract and end it with a semicolon because it has no body in this class.

Now, we must implement this abstract method in our derived classes, starting with the motorcycle.

C# 14.0
namespace AbstractClasses;
public class Motorcycle : Vehicle
{
public Motorcycle()
{
NumberOfWheels = 2;
}
// Must implement the method
public override void Accelerate()
{
Console.WriteLine("Motorcycle accelerating.");
}
}
  • Line 11: We use the override keyword to provide the concrete implementation for Accelerate.

  • Line 13: We define the implementation logic specific to the Motorcycle.

The car class must also implement the abstract method, but with logic specific to cars.

C# 14.0
namespace AbstractClasses;
public class Car : Vehicle
{
public int NumberOfSeatbelts { get; set; }
public Car()
{
NumberOfWheels = 4;
}
// Must implement the abstract method
public override void Accelerate()
{
Console.WriteLine("Car accelerating.");
}
}
  • Lines 13–16: We override Accelerate in the Car class to provide its own unique behavior.

Finally, we can run the program to see how each object handles the method call differently.

C# 14.0
using AbstractClasses;
Car car = new Car();
Motorcycle motorcycle = new Motorcycle();
car.Accelerate();
motorcycle.Accelerate();
  • Line 3–4: We instantiate the concrete classes.

  • Line 6: We call Car.Accelerate(), which prints “Car accelerating.”

  • Line 7: We call Motorcycle.Accelerate(), which prints “Motorcycle accelerating.”

We must use the override keyword when implementing an abstract method. Abstract classes can also define abstract events, properties, and indexers.

Note: If a derived class does not provide an implementation for inherited abstract members, it must also be declared abstract.