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.
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.
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.
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.
| Criterion | virtual method | abstract method |
|---|---|---|
| Has a body (default implementation) | Yes | No, signature only |
| Override in derived class | Optional (override) | Mandatory (override) |
| Allowed in | Any non-sealed class | Abstract classes only |
| Can call base implementation | Yes, base.Method() | Nothing to call |
| Instantiating the declaring class | Allowed | Not allowed (class is abstract) |
| Use when | A sensible default exists | Every 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.
