Back to Blog
C#

c# virtual keyword: How Overridable Members Work

Learn how the c# virtual keyword enables method overriding and polymorphism, including syntax, runtime dispatch, and common design pitfalls.

virtual methodspolymorphismmethod overridinginheritanceC# object-oriented programming
Diagram showing a base class method marked virtual being overridden by a derived class implementation.

The c# virtual keyword marks a method, property, event, or indexer as overridable in derived classes. When a member is declared virtual, calls to it are resolved at runtime based on the actual type of the object, not the declared type of the reference variable. That runtime resolution is what enables polymorphic behavior in C#.

Declaring a Virtual Member

The syntax is straightforward: place virtual before the member declaration in the base class.

public class DocumentExporter { public virtual string BuildFileName(string documentId) { return $"{documentId}.txt"; } }

BuildFileName has a default implementation that returns a .txt extension. Any derived class can replace that behavior with its own implementation.

Overriding a Virtual Member

A derived class uses the override keyword to replace the base implementation:

public class PdfExporter : DocumentExporter { public override string BuildFileName(string documentId) { return $"{documentId}.pdf"; } }

When code holds a DocumentExporter reference but the object is actually a PdfExporter, calling BuildFileName invokes the PdfExporter version:

DocumentExporter exporter = new PdfExporter(); string name = exporter.BuildFileName("report"); // name == "report.pdf"

The override keyword is required. A derived class cannot silently replace a virtual member without it. If you define a method with the same name and signature in a derived class without override, the compiler warns that the member hides the inherited one. You can suppress that warning with new, but the result is not polymorphic.

How Virtual Dispatch Works

When the JIT compiler compiles a call to a virtual method, it does not emit a direct call to a fixed implementation. Instead, it performs an indirect lookup through the type's virtual method table, commonly called the vtable. Each type that inherits a virtual member has its own vtable entry, and that entry points to the most derived implementation applicable to that type.

For a non-virtual method, the compiler resolves the target at compile time and emits a direct call. For a virtual method, the target is only known when the actual object type is known, which is at runtime. This is why virtual calls carry a small overhead compared to non-virtual calls: the lookup adds an indirection step.

Rules for Overriding

Several rules govern what can be virtual and how override works:

  • virtual can be applied to methods, properties, events, and indexers, but not to static members.
  • A virtual member cannot be private, because a derived class must be able to see and override it.
  • The override method must have the same signature and return type as the virtual method.
  • An override method is implicitly virtual unless it is declared sealed override.
  • Each override can be overridden again by further derived classes unless it is marked sealed override.

A sealed override stops further overriding:

public class PdfExporter : DocumentExporter { public sealed override string BuildFileName(string documentId) { return $"{documentId}.pdf"; } }

Now a class that inherits from PdfExporter cannot override BuildFileName again.

Virtual vs. Abstract vs. Interface

virtual provides a default implementation that derived classes may replace. abstract declares a member with no implementation at all, forcing every non-abstract derived class to provide one. An interface declares a contract without any implementation, and implementing classes must supply the behavior.

ApproachDefault implementationDerived class requirementUse case
virtualYesOptional overrideBase behavior with extension points
abstractNoMust overrideForced contract with shared base logic
InterfaceNoMust implementCapability contract across unrelated types

Use virtual when you have a sensible default and want to allow customization. Use abstract when every derived class must provide its own behavior but you still want a common base type. Use an interface when the contract matters more than the inheritance relationship.

Common Mistakes with Virtual Members

One frequent mistake is using new instead of override. The new keyword hides the base member rather than overriding it. Code that calls the method through a base-typed reference will invoke the base implementation, which is usually not what the developer intended.

Another mistake is calling a virtual method from a base constructor. During base class construction, the derived class constructor has not yet run. If the virtual method is overridden and the override depends on fields initialized in the derived constructor, those fields are still at their default values. The call may produce unexpected results or null reference exceptions.

public class BaseService { public BaseService() { Initialize(); // virtual call in constructor } public virtual void Initialize() { // default logic } } public class DerivedService : BaseService { private readonly string _connectionString; public DerivedService(string connectionString) { _connectionString = connectionString; } public override void Initialize() { _connectionString.Trim(); // NullReferenceException risk } }

The base constructor runs before _connectionString is assigned. Avoid virtual calls in constructors unless you fully control the override behavior.

Design and Performance Considerations

Virtual methods are a design decision, not a default. Every virtual member is an extension point that subclasses may override, which means the base class cannot assume its own implementation will always run. Overusing virtual makes a class harder to reason about and more difficult to maintain, because each override can change behavior in ways the base class author did not anticipate.

From a performance perspective, virtual calls are slightly more expensive than non-virtual calls because of the vtable indirection. In most application code the difference is negligible. It becomes relevant only in hot paths where millions of calls per second occur. If profiling shows that virtual dispatch is a bottleneck, options include marking the method sealed to allow the JIT to devirtualize the call in some runtime versions, or restructuring the design to avoid polymorphism in the hot path.

A more important consideration is whether virtual is the right tool for the job. If a method is only called through the base type and never overridden, making it virtual adds design surface without benefit. If a method is overridden in many places, consider whether an interface or abstract class better expresses the contract.

c# virtual keyword: How Overridable Members Work | RYUSLOG DEV