In this article, we will learn about the differences between a virtual and an abstract method.

Object-oriented programming thrives on inheritance – leveraging common functionalities across classes for efficient and organized code. However, regarding methods, C# offers two distinct approaches to defining behavior: virtual and abstract methods. While both enable polymorphism, they differ significantly in their implementation and usage. 

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Virtual And Abstract Method Overview

A virtual method is declared using the virtual keyword. It provides a default implementation that derived classes can optionally override and tailor to their specific needs. In this way, the base class establishes a common foundation of functionality while empowering subclasses to adapt and customize the behavior by overriding the virtual method’s default implementation.

An abstract method serves as a placeholder within abstract classes, declared with the abstract keyword. Unlike virtual methods, abstract methods don’t have default implementations and can only exist within abstract classes. Rather they serve as placeholders, prompting derived classes to provide their specific functionality, by using the override keyword.

Time to see virtual and abstract methods in action with a real-world example!

Virtual vs. Abstract

Let’s build a transport agency where users can select their rides from Car, Train, or Plane and get estimated travel time and base fare. 

For this example, we will create TransportAgency class with a CreateTransportMode() method to instantiate the appropriate transport object based on user choices:

public enum TransportModeType
{
    Car,
    Plane,
    Train
}
                                                                  
internal class TransportAgency
{
    public TransportMode CreateTransportMode(TransportModeType modeType)
    {
        return modeType switch
        {
            TransportModeType.Car => new Car(60),
            TransportModeType.Train => new Train(4),
            TransportModeType.Plane => new Plane(800),
            _ => throw new ArgumentException("Invalid transport mode type."),
        };
    }
} 

Now let’s create an abstract class named TransportMode that defines an abstract method GetTravelTime() and a virtual method CalculateBaseFare(): 

internal abstract class TransportMode
{
    public abstract double GetTravelTime(double distance);

    public virtual double CalculateBaseFare(double distance)
    {
        return distance * 0.5; 
    }
}

To determine the total travel time based on the mode of transportation, we use the method GetTravelTime(). This method is designed as abstract because travel time varies significantly depending on the transportation mode used. It wouldn’t be meaningful to provide a default travel time in this context.

Moving on to fare calculation, the CalculateBaseFare() method determines the base fare for each transportation mode. We made this method virtual so that we can provide a default fare that can be applied universally. Derived classes will have the option to either utilize this default fare directly in their total fare calculations or override the method to implement their specific fare calculation logic tailored to their unique needs.

Let’s see how we can use our abstract and virtual methods in derived classes.

Derived Classes

Let’s construct three classes – Car, Train, and Plane. These classes will inherit from our abstract class TransportMode. 

Let’s start!

First stop, the Car class:

internal class Car(double averageSpeed) : TransportMode
{
    private readonly double _averageSpeed = averageSpeed;

    public override double GetTravelTime(double distance)
    {
        return distance / _averageSpeed;
    }
   
    public override double CalculateBaseFare(double distance)
    {
        return base.CalculateBaseFare(distance) + 2.5; 
    }
}

We tailor fare calculations for cars by overriding the virtual method CalculateBaseFare() in the Car class. This method leverages the existing default base fare from the base class and then incorporates the fuel cost into the final calculation. We also override the abstract method GetTravelTime() to provide a car-specific implementation for determining travel time.

Next up, the Train class:

internal class Train(double fixedJourneyTime) : TransportMode
{
    private double _fixedJourneyTime = fixedJourneyTime;

    public override double GetTravelTime(double distance)
    {
        return _fixedJourneyTime; 
    }

    public override double CalculateBaseFare(double distance)
    {
        var baseFare = base.CalculateBaseFare(distance);

        if (distance > 500)
        {
            baseFare *= 0.9;              
        }

        return baseFare;
    }
}

Here, we override the virtual method CalculateBaseFare() in the Train class. This method leverages the default base fare from the parent class but also offers a 10% discount to encourage longer trips. We take a direct approach to implementing the abstract method GetTravelTime(), opting for a fixed travel time that remains constant regardless of distance.

And finally, the Plane class:

internal class Plane(double cruisingSpeed) : TransportMode
{
    private readonly double _cruisingSpeed = cruisingSpeed;

    public override double GetTravelTime(double distance)
    {
        return distance / _cruisingSpeed + 0.5; 
    }

    public override double CalculateBaseFare(double distance)
    {
        if (distance < 500)
        {
            return 100; 
        }
        else if (distance < 1000)
        {
            return 150; 
        }
        else
        {
            return distance * 0.2; 
        }
    }
}

In the Plane class, we break away from the base class’s default fare. Instead, we create an entirely new fare calculation strategy within our virtual method CalculateBaseFare(). To determine the total journey time, we implement the abstract GetTravelTime() method, factoring in both flight time and takeoff/landing periods.

Let’s run the code and check the fare and travel times for the transport mode Plane and travel distance 1500:

Select a transport mode:
1. Car
2. Train
3. Plane
4. Exit
Enter choice: 3
Enter travel distance (km): 1500

Estimated travel time: 2.375 hours
Base fare: $300

Now let’s recap the key takeaways from our code journey. We use the abstract class TransportMode to mandate that every derived class—Car, Train, or Plane—must override the abstract method GetTravelTime(). This ensures that every transport mode provides a way to calculate travel time based on distance, leading to consistency and predictability across all modes.

Virtual methods, on the other hand, introduce a degree of customization. Our virtual method CalculateBaseFare() serves as a starting point for fare calculations, but derived classes can tailor it to their specific needs. This allows us to incorporate fuel costs in the Car class and offer discounts for longer journeys in the Train class.

Key Differences Between a Virtual and an Abstract Method

Now that we’ve explored virtual and abstract methods in both theory and practice, let’s review their key differences to solidify our understanding:

FeatureVirtual MethodAbstract Method
ImplementationHas a default implemenationNo implementation
OverridingOptionalMandatory for non-abstract classes
LocationCan be declared in abstract and non-abstract classesCan only be declared in abstract classes
PurposeProvides a default behavior that can be overriddenMandatory for the derived classes to override the abstract methods
Incompatible keywordsstatic, abstract, private, and overridestatic and virtual
FlexibilityOffers greater flexibility for customizationEncourages consistency and uniformity

Conclusion

In this article, we saw the differences between virtual and abstract methods. Mastering the intricacies of virtual and abstract methods in C# empowers us to build robust and adaptable object-oriented applications. By understanding their distinct features and leveraging them judiciously, we can achieve cleaner code, enhanced polymorphism, and greater flexibility in our software designs.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!