Updated on

A virtual method in C# is a method that a derived class is allowed to override, and the runtime picks which version runs from the object’s actual type. Methods are non-virtual until we add the virtual keyword.

We’ll see how that works under the hood, how to implement it, when it’s worth it, and what a virtual call really costs on .NET 10.

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

Let’s dive in.

What Is a Virtual Method in C#?

A virtual method is a method that a derived class may replace with its own version. The base class marks it with the virtual keyword, and the derived class supplies the new version with override.

The runtime makes the choice while the program runs. It looks at the actual type of the object, not the declared type of the variable. So a Shape variable that holds a Circle runs the Circle version.

Overriding is optional. A derived class that doesn’t override the method simply keeps the base class behavior.

Methods in C# are non-virtual by default, and a non-virtual method can’t be overridden. We opt in, one member at a time.

There is no such thing as a virtual class in C#, though a class can be abstract or sealed. The virtual keyword applies to members only: methods, properties, indexers and events.

Virtual properties work the same way, and they are why EF Core’s lazy-loading proxies ask for virtual navigation properties, which we explain in virtual properties in EF Core.

We declare a virtual method by adding the virtual keyword before the return type:

public virtual double CalculateArea()

We cannot use the virtual keyword when the method:

  • is static (C# 11 added static virtual members, but only on interfaces)
  • is abstract (an abstract method has no body to inherit, and we compare the two in how virtual and abstract methods differ)
  • is a constructor or an accessor (get and set)
  • already overrides the virtual method from the base class
  • is a new member of a sealed class
  • is private

Late Binding

C# implements two types of bindings: early binding and late binding.

Early binding, also known as compile-time binding, resolves implementing the method during compile time and is used for non-virtual methods.

On the other hand, late binding is known as run-time polymorphism. This is a programming concept where we determine the actual implementation of a method at run-time. In other words, we resolve the specific implementation of the method during program execution based on the actual type of the object. We achieve late binding using virtual methods.

To learn more about polymorphism and its types, please read Polymorphism in C#.

Dynamic Dispatch

When a class in .NET contains a virtual method, the runtime builds a virtual method table (v-table) for that class when it loads the type. The v-table contains a list of pointers to the virtual methods in the class. We use these pointers whenever we invoke a virtual method from the instance.

The pointers identify the applicable method implementation because we may not know whether to call the base or derived class’s method during compile time. We call this process a dynamic dispatch, and its responsibility is calling the correct implementation of the virtual method at run-time based on the actual type rather than the declared type of the object.

Microsoft’s C# reference describes the same rule from the caller’s side: “When you invoke a virtual method, the runtime checks the type of the object for an overriding member.”

How Do We Implement Virtual Methods in C#?

Let’s define a base class Shape with two virtual methods and one non-virtual method:

public class Shape
{
    public virtual double CalculateArea()
    {
        return 0;
    }

    public virtual string GetShapeType()
    {
        return "This is a generic shape";
    }

    public string Draw()
    {
        return "Drawing a generic shape";
    }
}

Next, we will define two derived classes to inherit from the base class to demonstrate how they interact with the base class. To learn more about inheritance, read inheritance in C#.

Let’s define the Rectangle class:

public class Rectangle : Shape
{
    public double Width { get; set; }

    public double Height { get; set; }

    public override double CalculateArea()
    {
        return Width * Height;
    }

    public override string GetShapeType()
    {
        return "This is a rectangle";
    }

    public new string Draw()
    {
        return "Drawing a rectangle";
    }
}

And the Circle class:

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

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }

    public new string Draw()
    {
        return "Drawing a circle";
    }
}

The CalculateArea() method in the derived classes overrides the virtual method of the base class and provides its implementation. Please visit Method Overriding in C# to learn more about method overriding.

However, we cannot override a non-virtual method. The next snippet doesn’t compile, because Draw() isn’t virtual:

public override string Draw()
{
    return "Drawing a circle";
}

If we try to do so, we get an error message showing we cannot override the Draw() method because it is not marked virtual:

'Circle.Draw()': cannot override inherited member 'Shape.Draw()' because it is not marked virtual, abstract, or override

If we drop the override keyword instead, the code compiles, but with a warning:

public string Draw()
{
    return "Drawing a circle";
}

The compiler will show us the warning that this implementation hides the inherited method and suggest adding a new keyword:

'Circle.Draw()' hides inherited member 'Shape.Draw()'. Use the new keyword if hiding was intended.

Let’s follow the suggestion and use the new keyword. This keyword will instruct the compiler that we intentionally hide the method from the base class:

public new string Draw()
{
    return "Drawing a circle";
}

After successfully creating the base and derived classes, let’s start using the classes.

Firstly, let’s instantiate all three classes and call their CalculateArea() methods to demonstrate method overriding:

var shape = new Shape();
var circle = new Circle() { Radius = 2 };
var rectangle = new Rectangle() { Height = 2, Width = 3 };

Console.WriteLine($"Shape area: {shape.CalculateArea()}");         // Shape area: 0
Console.WriteLine($"Circle area: {circle.CalculateArea()}");       // Circle area: 12.566370614359172
Console.WriteLine($"Rectangle area: {rectangle.CalculateArea()}"); // Rectangle area: 6

The CalculateArea() method in the derived classes will override the method with the same signature in the base class. This results in different outputs for each call, as described in the code comments. The decimal separator follows our machine’s culture, so on some machines the circle area prints with a comma.

Note that overriding a virtual method is not mandatory. If the derived class doesn’t override the virtual method, the method implementation from the base class will be executed.

Let’s try to call the GetShapeType() method, which we override in the Rectangle class, but not in the Circle class:

Console.WriteLine($"Shape type: {shape.GetShapeType()}");         // Shape type: This is a generic shape
Console.WriteLine($"Circle type: {circle.GetShapeType()}");       // Circle type: This is a generic shape
Console.WriteLine($"Rectangle type: {rectangle.GetShapeType()}"); // Rectangle type: This is a rectangle

Next, let’s try to call the Draw() method on the base and derived classes, but this time we will declare Circle class as Shape:

Shape circleAsShape = new Circle() { Radius = 2 };
Console.WriteLine($"Shape draw: {shape.Draw()}");          // Shape draw: Drawing a generic shape
Console.WriteLine($"Circle draw: {circleAsShape.Draw()}"); // Circle draw: Drawing a generic shape
Console.WriteLine($"Rectangle draw: {rectangle.Draw()}");  // Rectangle draw: Drawing a rectangle

The compiler resolves non-virtual methods based on the declared type, not the actual type. The output shows that the Circle class executes the method from the base class, while the Rectangle class executes the derived class implementation.

When Should We Use Virtual Methods in C#?

We make a method virtual when we expect a derived class to change that one behavior, and we want it to.

Frameworks and class libraries are the typical case. The base class ships the general behavior, and the people using the library customize one step by overriding it, without touching the base class. That is the open/closed principle, part of the SOLID principles, in its simplest form.

Testing is the second case. Mocking libraries such as Moq create a derived class at run time and override members to return the values a test needs. They can only replace members that are virtual, abstract or declared on an interface.

Everything else can stay non-virtual. Each virtual method is a promise that the class still works when someone replaces that method, and that promise is hard to keep for internal helpers.

Here is an example of how we can override virtual methods while mocking classes in tests. We will use the Moq testing library to mock in this case. In an MSTest test project that references our project, we add the Moq package, put using Moq; at the top of the test file, and write the method inside its [TestClass] class:

[TestMethod]
public void WhenCircleClassGetShapeTypeMock_ThenCircleClassGetShapeTypeResult()
{
    var circleMock = new Mock<Circle>();
    circleMock.Setup(p => p.GetShapeType()).Returns("This is a circle");
    var circleType = circleMock.Object.GetShapeType();

    Assert.AreEqual("This is a circle", circleType);
}

Such a setup isn’t possible if the method is non-virtual. The next line compiles, but it throws when the test runs:

circleMock.Setup(p => p.Draw()).Returns("Drawing a circle");

We will receive an error when we debug the test in Visual Studio:

An exception of type 'System.NotSupportedException' occurred in Moq.dll but was not handled in user code
Unsupported expression: p => p.Draw()
Non-overridable members (here: Circle.Draw) may not be used in setup / verification expressions.

Polymorphism enables code reusability, as derived classes do not reimplement the behavior of the base class if it is the same.

By providing the typical implementation in the base class, we define the general behavior of the class. The derived classes can implement specialized and customized behavior. Such a concept promotes the separation of concerns.

Despite numerous benefits, we should use virtual methods carefully. Several levels of overriding can lower code readability.

Are Virtual Method Calls Slow in .NET?

Rarely enough to matter. A virtual call needs one extra step, reading the method’s address from the object’s method table, and the JIT can’t inline a call when it doesn’t know the target.

On .NET 10 the JIT removes much of that cost. When it can prove the exact type of the object, it calls the method directly, and calling through a sealed type gives it that proof. When it can’t prove the type, Dynamic PGO, on by default since .NET 8, watches which types actually reach the call and adds a fast path for the common one.

In our benchmark, a thousand virtual calls on one type ran as fast as the non-virtual version, and so did the calls on a sealed type. Mixing two types at the same call site cost 40 to 82 percent more, depending on the run.

Interfaces and delegates are not faster alternatives. The interface call matched the virtual one, and the delegate was about 25 percent slower.

To measure it, we create a separate console project with the BenchmarkDotNet package and trimmed versions of our classes. There we add a non-virtual twin of CalculateArea() to Circle, a sealed copy of the class, and a small IShape interface that Shape implements:

public interface IShape
{
    double CalculateArea();
}

public class Shape : IShape
{
    public virtual double CalculateArea() => 0;
}

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

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

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

public sealed class SealedCircle : Shape
{
    public double Radius { get; init; }

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

Each benchmark loops over 1,000 objects and adds up their areas. Here are two of the six, the non-virtual baseline and the virtual call through a Shape array:

[Benchmark(Baseline = true)]
public double NonVirtual()
{
    var total = 0.0;
    foreach (var circle in _circles)
        total += circle.CalculateAreaNonVirtual();
    return total;
}

[Benchmark]
public double VirtualOneType()
{
    var total = 0.0;
    foreach (var shape in _circlesAsShapes)
        total += shape.CalculateArea();
    return total;
}

The other four follow the same shape: an array that mixes Circle and Rectangle, an array of SealedCircle, the circles behind IShape, and a Func<double> per circle. The full class and the trimmed Rectangle are in the repo.

CallWhat the loop callsTime for 1,000 callsCompared with non-virtualWith Dynamic PGO off
Non-virtualCircle.CalculateAreaNonVirtual()760 nsbaselinebaseline
Virtual, one typeShape.CalculateArea(), every object a Circle763 ns1.00x2.06x
Virtual, two typesShape.CalculateArea(), Circle and Rectangle alternating1,287 ns1.69x2.21x
Virtual, sealed typeSealedCircle.CalculateArea()756 ns0.99x1.00x
InterfaceIShape.CalculateArea(), every object a Circle766 ns1.01x2.39x
Delegatea Func<double> bound to each circle940 ns1.24x2.39x

BenchmarkDotNet 0.15.8, .NET 10.0.10, AMD Ryzen 5 3600, Windows 10. Lower is better. The last column is a second run with the environment variable DOTNET_TieredPGO=0. The two-type row moved between 1.40x and 1.82x from run to run.

The last column shows where the speed comes from. With Dynamic PGO switched off, the one-type virtual loop takes twice as long as the non-virtual one, and the interface and delegate loops slow down the same way. The sealed loop doesn’t move, because the JIT never needed a guess for it.

The two-type row is different. Once a call site sees several types, the JIT’s fast path catches fewer of the calls, and the cost shows up. Even then it is about 0.3 to 0.6 nanoseconds per call on our machine, so it matters in a hot loop over millions of objects and almost nowhere else.

So we don’t need to avoid virtual methods in performance-critical code. We measure first, and when a profile does point at a virtual call, sealing the classes that aren’t meant to be inherited, and calling them through their own type, is the cheapest fix. We cover that in sealed classes and performance, and the setup for runs like ours in benchmarking with BenchmarkDotNet.

Conclusion

Virtual methods in C# are an essential feature of object-oriented programming. They allow for polymorphism, late binding, and dynamic method dispatch, providing flexibility and extensibility in class hierarchies.

Proper use of virtual methods can result in clean and maintainable code. And on .NET 10 the cost of a virtual call is rarely a reason to avoid one: we measure first, and seal what isn’t meant to be inherited.

Tested with .NET 10.0.10, MSTest 4.5.1, Moq 4.21.0 and BenchmarkDotNet 0.15.8.