Updated on

A virtual method has a default implementation that derived classes may override; an abstract method has no implementation, and derived classes must provide one. That single difference drives everything else: abstract methods can only live in abstract classes, while virtual methods can live anywhere and still work if nobody overrides them.

We choose virtual when a sensible default exists and abstract when forcing every subclass to decide is the point.

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.

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 in C#, 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.

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.

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

Derived Classes

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

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 readonly 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.

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

What Is the Difference Between Virtual and Abstract Methods in C#?

The difference comes down to whether a default implementation exists. A virtual method is declared with a body; the base class provides working behavior, and a derived class may override it with the override keyword, or inherit the default silently.

An abstract method is a contract without a body: it declares the signature and forces every non-abstract derived class to implement it, which is why the compiler only allows abstract methods inside abstract classes: a class with unimplemented methods must never be instantiated. Overriding is optional for virtual methods and mandatory for abstract ones.

A virtual override can still reach the original behavior through base.Method(), while an abstract method has nothing to call. The C# reference is blunt about the relationship: “An abstract method is implicitly a virtual method.” Both enable runtime polymorphism through the same mechanism, and in practice they combine: one abstract class often declares abstract methods for behavior that must vary and virtual methods for behavior with a reasonable default.

Criterionvirtual methodabstract method
Has a body (default implementation)YesNo, signature only
Override in derived classOptional (override)Mandatory (override)
Allowed inAny non-sealed classAbstract classes only
Can call base implementationYes, base.Method()Nothing to call
Instantiating the declaring classAllowedNot allowed (class is abstract)
Use whenA sensible default existsEvery subclass must decide

The row is a language rule, not a convention: the C# reference states that “Abstract method declarations are only permitted in abstract classes.”

When Should We Use a Virtual Method vs an Abstract Method?

We ask one question: does a correct default implementation exist? If yes, the method should be virtual. In our transport example, CalculateBaseFare() is virtual because distance-based pricing is a reasonable default. Car and Train refine it, and a future Ferry class could ship without touching fares at all.

If no default makes sense, the method should be abstract. GetTravelTime() is abstract because a “default travel time” is meaningless; any value we invented would be silently wrong for some transport mode, and a compile error in each subclass is better than a wrong number in production.

Two more signals help. If we find every derived class overriding a virtual method anyway, the default was never real. Make it abstract. And if the base class needs no state and no defaults at all, we should consider an interface instead of an abstract class.

We can dig deeper into that trade-off with default interface methods, or step back further and weigh composition vs inheritance before committing to either pattern.

Conclusion

The decision rule is simple: choose virtual when a sensible default implementation exists, and choose abstract when every subclass must decide for itself. Everything else in this article follows from that one distinction.

Tested with .NET 10.0.10.