BrandGhost
Polymorphism in C#: What Runs Through Base and Interface References?

Polymorphism in C#: What Runs Through Base and Interface References?

The examples separate these call-resolution questions:

Case What matters
Virtual member overridden by a derived class Runtime implementation for that virtual slot
Member hidden with new The member selected through the call-site type
Explicit interface implementation Calling through the relevant interface contract
Static interface contract Its generic/static context, not ordinary instance virtual dispatch

Polymorphism in C# can be confusing when two variables refer to the same object but a call behaves differently through each variable. The missing piece is often the difference between the reference's compile-time type and the object's runtime type.

This article traces those calls directly. You will see why overriding a virtual member differs from hiding a member with new, why explicit interface implementations are called through a particular interface, and why static interface contracts need a separate explanation.

Example scope: The three code blocks are independent, complete console programs intended for .NET 10. They have not been compiled or executed. Every predicted result below is documentation-based reasoning, not captured execution output.

Two types participate in polymorphism in C#

A variable has a type available to the compiler. An object created at runtime also has its concrete type. Those can differ when a derived object is referenced through a base-class variable or an implementing object is referenced through an interface.

Microsoft's polymorphism guide explains both treating derived objects as base objects and selecting overrides of virtual members at runtime. These are related ideas, but they are not permission to call every concrete member through every reference.

Start by examining the expression to the left of the dot. Its compile-time type tells you what member surface the call is using. Then ask whether the selected member participates in virtual dispatch or another documented dispatch mechanism.

The object is not replaced when another variable refers to it. Microsoft's object-reference documentation describes multiple variables referring to the same class instance. Changing the reference type changes the view available at the call site, not the identity of the object.

Keeping this distinction visible prevents vague explanations such as “the newest method runs” or “the base variable always uses the base method.” Each can be wrong depending on the member involved.

This article focuses on ordinary instance calls with the declared types shown in the examples. It does not introduce dynamic, reflection, extension-method selection, or a performance comparison. Those have separate questions and would obscure the dispatch rules being illustrated.

Virtual and override describe one extension point

A virtual base member provides an extension point in a hierarchy. A derived class can override that member. The selected virtual member still belongs to the same chain of overriding behavior.

The following example uses message rendering rather than geometric relationships. A decorated renderer supplies a specialized rendering body. The program calls it through both the concrete and base reference.

using System;

namespace OverrideExample;

internal static class Program
{
    private static void Main()
    {
        var concrete = new DecoratedRenderer();
        MessageRenderer throughBase = concrete;

        Console.WriteLine(concrete.Render());
        Console.WriteLine(throughBase.Render());
        Console.WriteLine(ReferenceEquals(concrete, throughBase));
    }
}

public class MessageRenderer
{
    public virtual string Render()
    {
        return "message";
    }
}

public sealed class DecoratedRenderer : MessageRenderer
{
    public override string Render()
    {
        return $"[{base.Render()}]";
    }
}

Reasoning from the documented virtual-dispatch rule, both rendering calls reach DecoratedRenderer.Render. The base-reference call does not force the base implementation merely because the variable is declared as MessageRenderer.

Inside the override, base.Render() explicitly invokes the base body. The decoration uses that result. The base body does not run automatically before or after every override; it runs here because the implementation asks for it.

Microsoft's override and new guidance documents both the override relationship and using base to reach base behavior. This distinction matters when reading an override that replaces, rather than extends, the base operation.

The snippet's contract is intentionally small: return a message string. It does not model an extensible UI framework or claim that every derived renderer respects a larger application contract. The example isolates which body is selected.

For an introduction to the hierarchy syntax itself, the existing inheritance examples in C# cover that foundation. Here, the important observation is that a base reference can still invoke a derived override.

New hiding introduces a different member

The new modifier expresses hiding, not participation in the same override chain. Microsoft's polymorphism documentation explicitly distinguishes hiding by compile-time reference type from virtual override selection by runtime object.

The next program makes that distinction visible. The hidden renderer declares a new, nonvirtual method with the same name as the virtual base method. It does not override the base extension point.

using System;

namespace HidingExample;

internal static class Program
{
    private static void Main()
    {
        var concrete = new HiddenRenderer();
        MessageRenderer throughBase = concrete;

        Console.WriteLine(concrete.Render());
        Console.WriteLine(throughBase.Render());
        Console.WriteLine(ReferenceEquals(concrete, throughBase));
    }
}

public class MessageRenderer
{
    public virtual string Render()
    {
        return "base message";
    }
}

public sealed class HiddenRenderer : MessageRenderer
{
    public new string Render()
    {
        return "hidden message";
    }
}

The concrete-reference call selects HiddenRenderer.Render. The base-reference call selects the base virtual member. Since this derived class supplies no override of that member, the base implementation supplies its behavior.

Both variables still refer to the same object. The differing selection is about the member surface and override relationship, not two separate renderer instances.

This is why changing override to new is not merely silencing a warning. It changes the relationship between the members. A consumer using a base reference does not start calling the new hidden member simply because the runtime object has that declaration.

Microsoft also documents a warning when a same-named derived method omits the relevant modifier and behaves as hiding. Adding new acknowledges the hiding intention; it does not turn the behavior into overriding.

Hiding can be relevant when names collide during library evolution. Its benefit is making the independent member intention explicit. Its cost for readers is that the same spelling can describe different calls depending on the reference type. For a single intended extension point, overriding communicates a different relationship.

These are language and readability tradeoffs, not a blanket instruction to redesign every existing hierarchy. First establish what the caller expects. Then determine whether the code expresses that relationship.

Member selection comes before the override body

The simple examples use one parameterless method name, which keeps the call unambiguous. Real code can include overloaded methods with compatible arguments. In that situation, the compiler must select a member before virtual dispatch can determine its implementation.

Microsoft's method-selection explanation includes an example where a derived overload and an inherited virtual member affect selection. The key lesson is not to memorize that one overload combination, but to avoid conflating overload selection with override selection.

When investigating a surprising call, write down the reference type, the argument types, and the candidate members. Only after identifying the selected virtual member should you ask which override runs.

A cast to a base type can change the surface used for member selection without changing the object itself. For an already selected virtual member, the runtime override rule still applies. A cast is therefore not a general instruction to “run the base implementation.”

The explicit base call inside the first example serves a different purpose from assigning an object to a base-typed variable. The former reaches the base implementation from derived code. The latter gives client code a base-typed reference.

Careful terminology saves time here. “Declared type,” “selected member,” and “override implementation” each identify a different part of the explanation. “It calls the derived method” is incomplete unless you know which member was selected first.

Interface references describe roles

An interface can describe an operation that multiple types provide. The consumer calls through the role rather than depending on one concrete class's entire API.

The existing article on using interfaces effectively explains that boundary in a broader OOP context. Dispatch adds another question: which implementation supplies the interface member on this object?

A class can supply an ordinary public method for an interface member, or it can implement a member explicitly. Explicit implementation is useful for keeping a role-specific operation off the ordinary concrete surface or distinguishing roles with the same signature.

Microsoft states that an explicit interface implementation is called only through the specified interface. It also documents that the implementation has no access modifier.

This is not equivalent to a public method that you happen to call through an interface. The following example deliberately gives two roles separate implementations while preserving an independently named concrete operation.

using System;

namespace ExplicitRoleExample;

internal static class Program
{
    private static void Main()
    {
        var summary = new ReservationSummary();
        IDisplayText display = summary;
        IAuditText audit = summary;

        Console.WriteLine(summary.Describe());
        Console.WriteLine(display.GetText());
        Console.WriteLine(audit.GetText());
    }
}

public interface IDisplayText
{
    string GetText();
}

public interface IAuditText
{
    string GetText();
}

public sealed class ReservationSummary : IDisplayText, IAuditText
{
    public string Describe()
    {
        return "Reservation summary";
    }

    string IDisplayText.GetText()
    {
        return "Your reservation is confirmed.";
    }

    string IAuditText.GetText()
    {
        return "reservation:confirmed";
    }
}

The display-reference call reaches the display implementation; the audit-reference call reaches the audit implementation. The same object supplies both roles. The role being used determines which explicitly implemented interface member is called.

There is no ordinary public GetText method on ReservationSummary in this example. A direct summary.GetText() call is therefore not offered by its concrete API. The program calls Describe directly and obtains role-specific text through the corresponding interface references.

Explicit implementation can clarify separate capabilities. Its tradeoff is making callers use the interface view for those members. If the operation genuinely belongs on the ordinary concrete API, a public implementation may be easier to discover.

The idea of focused interfaces is relevant: roles should explain consumer needs, not just hide a large set of arbitrary methods behind different names.

Modern interfaces are not implementation-free declarations

Current Microsoft interface documentation includes default implementations, static members, and static abstract or virtual contracts. Therefore, an explanation that all interface members are implementation-free misses important language capabilities.

Default interface members also have an access surface worth understanding. Microsoft documents that a default member is accessed through an interface reference rather than appearing automatically as an ordinary member of the implementing class.

This article does not present default implementation as a requirement. It is a capability to recognize when reading modern code. A basic interface whose implementing class supplies the behavior remains a useful teaching example.

Static abstract and static virtual interface members need a separate mental model. The current documentation describes resolution using compile-time type information, without a runtime dispatch mechanism analogous to class virtual methods. They are not instance calls selecting an override from a base-reference object's runtime type.

That distinction is especially important when encountering generic contracts. A type parameter can supply a required static operation, but that does not turn the operation into the instance override chain illustrated by the renderer.

Avoid extending the phrase “runtime polymorphism” until it covers mechanisms with different selection rules. Name the feature being used. The benefit is a more accurate explanation, even if it takes one extra sentence.

Dispatch correctness is not contract correctness

Knowing which body runs does not tell you whether that body's behavior is appropriate. A renderer might return a string yet violate expectations about formatting or failure. A role implementation might compile but surprise its consumer.

The existing Liskov substitution article explores behavioral compatibility. That is the place for deeper substitution reasoning rather than repeating another geometry-based example here.

Similarly, dispatch is not a reason to create an interface for every class. The discussion of interface drawbacks provides useful context for balancing a meaningful boundary against added indirection.

The snippets here deliberately promise very little. They isolate method selection and role access so you can reason about a call. Application-level contracts would need additional documentation and independently checked behavior.

Frequently asked questions

What is polymorphism in C#?

For the instance examples here, it is using a common base or interface surface while an object supplies behavior through that surface. Virtual overrides and interface implementations are important mechanisms. Understanding a specific call requires identifying the reference type and the selected member, not only the object's concrete type.

Does a base reference always call the base implementation?

No. For a selected virtual member with a derived override, Microsoft documents runtime selection of that override. The first renderer example illustrates that relationship. The base implementation is explicitly reached by base.Render() inside the override, not forced by assigning the object to a base variable.

What is the difference between new and override?

override participates in an inherited virtual member's extension point. new hides a member with an independent declaration. In the hiding example, the concrete and base references select different members. The distinction is behavioral, not just a difference in compiler-warning appearance.

Can an explicit interface member be called directly on the class?

Not as an ordinary member through the concrete reference shown here. Microsoft documents access through the specified interface. The summary example uses interface references to choose display and audit roles, while its separately declared Describe method remains available directly.

Can interfaces contain implementations?

Yes. Current C# documentation includes default member implementations and static functionality. The basic role examples in this article omit those features to keep dispatch visible, not because implementation is forbidden. Explicit and default interface members also require attention to their interface-based access surface.

Do static abstract interface members use class virtual dispatch?

Not according to the current Microsoft interface reference. It describes resolution using compile-time type information rather than an analogous instance runtime override mechanism. Keep that feature separate from the renderer examples, which select behavior for a virtual instance member on a runtime object.

Trace the call in the right order

Polymorphism in C# becomes less mysterious when the explanation begins at the call site. Identify the reference type, determine the selected member, and then apply the dispatch rule for that member.

An override, a hidden member, and an explicit interface implementation can share similar syntax while expressing different relationships. Precise names and small examples make those differences visible. Contract compatibility remains a separate responsibility after you know which implementation runs.

What Makes Good Code? - Should Every Class Have An Interface? Pt 1

Are you interested in learning about interfaces in C#? This article explains the idea behind a C# interface by showing examples of interfaces in code.

Object-Oriented Programming in C#: State, Contracts, and Collaboration

Learn object-oriented programming in C# through state, contracts, and collaboration, with clear examples of encapsulation, inheritance, and composition.

Interface Segregation Principle C#: Focused Interfaces That Scale

Master the interface segregation principle c# with before/after examples, DI registration in .NET 10, and tips for designing focused, role-based interfaces.

An error has occurred. This application may no longer respond until reloaded. Reload