Enabling Polymorphism with the virtual Keyword in .NET

Allowing Derived Classes to Customize Behavior

Myth: virtual methods are slow and should be avoided. Reality: they're the foundation of polymorphism, letting you write code that works with base class references but executes derived class implementations. The performance cost is negligible compared to the flexibility you gain.

Mark a method virtual to allow derived classes to override it with custom implementations. When you call a virtual method through a base class reference, the runtime dispatches to the most-derived override. This lets subclasses specialize behavior while base classes define the interface.

You'll build a payment processing system where different payment types override virtual methods to implement their specific logic. By the end, you'll know when virtual methods enable clean designs and when they introduce unnecessary complexity.

Defining Virtual Methods

Add the virtual keyword to methods you want subclasses to customize. Derived classes use override to provide their implementation. The signature must match exactly.

PaymentBase.cs
public class Payment
{
    public decimal Amount { get; set; }

    public virtual void Process()
    {
        Console.WriteLine($"Processing payment of ${Amount}");
    }

    public virtual bool Validate()
    {
        return Amount > 0;
    }
}

public class CreditCardPayment : Payment
{
    public string CardNumber { get; set; }

    public override void Process()
    {
        Console.WriteLine($"Charging card {CardNumber} for ${Amount}");
    }

    public override bool Validate()
    {
        return base.Validate() && !string.IsNullOrEmpty(CardNumber);
    }
}

The base class defines virtual Process and Validate methods. CreditCardPayment overrides both to add card-specific logic. Calling Process on a Payment reference executes the CreditCardPayment version if that's the runtime type.

Polymorphism in Action

Work with base class references but get derived class behavior. The runtime resolves which implementation to call based on the actual object type, not the reference type.

PolymorphismDemo.cs
public class PayPalPayment : Payment
{
    public string Email { get; set; }

    public override void Process()
    {
        Console.WriteLine($"Sending ${Amount} via PayPal to {Email}");
    }
}

public class PaymentProcessor
{
    public void ProcessPayments(List<Payment> payments)
    {
        foreach (var payment in payments)
        {
            if (payment.Validate())
            {
                payment.Process();
            }
        }
    }
}

// Usage
var processor = new PaymentProcessor();
var payments = new List<Payment>
{
    new CreditCardPayment { Amount = 100, CardNumber = "1234" },
    new PayPalPayment { Amount = 50, Email = "user@example.com" }
};

processor.ProcessPayments(payments);

ProcessPayments works with Payment references. The virtual method dispatch ensures each payment type runs its specific implementation. You can add new payment types without changing ProcessPayments. This is polymorphism's power.

Calling Base Implementation

Use base to call the parent class implementation from an override. This lets you extend behavior without replacing it entirely.

BaseCall.cs
public class LoggedPayment : Payment
{
    public override void Process()
    {
        Console.WriteLine($"[LOG] Starting payment processing");
        base.Process();
        Console.WriteLine($"[LOG] Payment processing complete");
    }
}

LoggedPayment adds logging around the base implementation. Using base.Process() calls the parent's version, then the override adds behavior before and after. This is cleaner than duplicating the base logic.

Try It Yourself

Build a shape hierarchy that uses virtual methods for area calculation. This demonstrates how polymorphism lets you work with different shapes uniformly.

Steps

  1. Create: dotnet new console -n VirtualDemo
  2. Navigate: cd VirtualDemo
  3. Edit Program.cs
  4. Update .csproj
  5. Run: dotnet run
Program.cs
var shapes = new Shape[]
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};

foreach (var shape in shapes)
{
    Console.WriteLine($"{shape.GetType().Name} area: {shape.GetArea():F2}");
}

abstract class Shape
{
    public abstract double GetArea();
}

class Circle : Shape
{
    public double Radius { get; init; }

    public override double GetArea() => Math.PI * Radius * Radius;
}

class Rectangle : Shape
{
    public double Width { get; init; }
    public double Height { get; init; }

    public override double GetArea() => Width * Height;
}
VirtualDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Console

Circle area: 78.54
Rectangle area: 24.00

Design Trade-offs

Choose virtual methods when you have a stable abstraction with varying implementations. If you're building a framework or library where users extend your classes, virtual methods provide extension points. They work well for template methods, hooks, and behavior variation within a type hierarchy.

Choose composition with interfaces when implementations differ completely or when you need to swap behavior at runtime without inheritance. Interfaces give you flexibility to combine behaviors from multiple sources. Virtual methods lock you into single inheritance.

If unsure, start with non-virtual methods and add virtual when you discover extension points. Premature virtualization adds complexity and performance cost without benefit. Make methods virtual when you have concrete evidence that subclasses need customization.

Frequently Asked Questions

When should I mark methods virtual?

Mark methods virtual when you expect subclasses to customize behavior. If the method defines an extension point for derived classes, make it virtual. Leave methods non-virtual when the implementation must stay unchanged or when performance is critical and dynamic dispatch would hurt.

What's the gotcha with virtual methods in constructors?

Never call virtual methods from constructors. The override runs before the derived class constructor finishes, accessing potentially uninitialized fields. This causes null references and incorrect state. Use initialization methods called after construction or template methods that execute after construction completes.

How does sealed affect virtual methods?

Sealing a virtual method prevents further overriding in derived classes. The method stays polymorphic up to that point but locks after. Use sealed override to stop inheritance chains when a particular implementation should be final. This preserves performance while allowing limited extensibility.

Back to Articles