In this article, we will discuss virtual methods in C#. We will define what they are and how we implement them. We will also mention virtual methods’ uses, benefits, and drawbacks.
Let’s dive in.
What Are Virtual Methods in C#?
Virtual methods are a fundamental concept of object-oriented programming. They allow their behavior to be overridden in a derived class by a method with the same signature. The overriding is optional, and the derived class can still use the base class’s behavior.
Methods are, by default, non-virtual and cannot be overridden. We declare virtual methods by adding the virtual
keyword in the method signature before the method’s return type. They may or may not have a return type:
public virtual double CalculateArea()
We cannot use the virtual
keyword when the method:
- is static
- is abstract
- is a constructor or an accessor (get and set)
- already overrides the virtual method from the base class
- is sealed (or the class is sealed)
- is private
Late Binding
C# implements two types of bindings: early binding and late binding.
Early binding, also known as static binding or 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 dynamic binding or 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 compiler automatically creates a virtual method table (v-table) for that class. 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.
Virtual Methods Implementation
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 C# Intermediate – Inheritance.
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. Let’s see what happens if we try to override this non-virtual method:
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 'Circle.Draw()' because it is not marked virtual, abstract, or override
If we try to leave the same method as is without the override
keyword:
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.
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.
Use, Advantages, and Drawbacks of Virtual Methods in C#
We often implement virtual methods when developing frameworks, class libraries and testing. This enables extensions and customization of these classes without changing the behavior of the base class, allowing the open/closed principle, which is a part of SOLID principles. It also enables testability by creating the mock objects of the base classes.
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:
[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 is not possible if the method is non-virtual. If we try to override the non-virtual method in our test:
circleMock.Setup(p => p.Draw()).Returns("Drawing a circle");
We will receive an error during the test execution:
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.
Late binding can impact method invocation performance, as the run-time needs to look up in the v-table to determine the actual implementation of the virtual method.
In most scenarios, these overheads are not noticeable. But we should avoid using virtual methods in performance-critical code and tight loops. Instead, we should consider a different approach, like using interfaces or delegates.
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. However, despite their advantages, we should consider performance implications when using virtual methods.