Back to Blog
C#

C# Base Type Reference to Derived Object

c# base type reference derived object: Learn how a C# base type reference can hold a derived object, how virtual dispatch works, and when to cast or use pattern matching.

C#polymorphismtype castinginheritancevirtual methods
Diagram showing a base class reference pointing to a derived class instance in C#.

c# base type reference derived object requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you store a derived object in a C# base type reference, the compiler treats the variable as the base type, but the runtime object remains the derived type. This is the foundation of polymorphism in C#. For example:

class Animal { public virtual string Speak() => "Some sound"; } class Dog : Animal { public override string Speak() => "Woof"; } Animal pet = new Dog();

The variable pet is statically typed as Animal, but at runtime it holds a Dog instance. The compiler only exposes members declared in Animal, but the actual object type drives virtual method calls.

Assigning a Derived Object to a Base Type Reference

The assignment Animal pet = new Dog(); works because Dog inherits from Animal. This is an implicit upcast. It is always safe because a derived object is a kind of base object. The reverse, downcasting from base to derived, requires an explicit conversion.

This pattern is common when you want to treat multiple derived types uniformly. For instance, you can store a List<Animal> that contains dogs, cats, and birds, and call common methods on each element without knowing its exact type.

How Virtual Method Dispatch Works

When you call a virtual method on a base type reference, the runtime selects the implementation based on the actual object type, not the reference type. In the example, pet.Speak() returns "Woof" because the runtime type is Dog.

This behavior is called virtual dispatch. The runtime uses a method table to locate the most derived override. The cost is a small indirection compared to a non-virtual call. In most applications, this overhead is negligible, but in tight loops with millions of calls, it can become measurable.

If a method is not virtual, the compiler resolves it at compile time using the reference type. Calling a non-virtual method on a base reference always executes the base implementation, even if the derived type defines a method with the same name using the new keyword. Mark methods as virtual when you intend to override behavior.

Accessing Derived Members Through a Base Reference

A base type reference only exposes members declared in the base type. To access a member that exists only on the derived type, you must convert the reference. The as operator performs a safe cast:

Dog dog = pet as Dog; if (dog != null) { dog.Fetch(); }

Alternatively, a direct cast with parentheses throws InvalidCastException if the runtime type is incompatible:

Dog dog = (Dog)pet;

Direct casting is appropriate when you are certain about the runtime type, but as is safer when the type might vary.

Pattern matching combines a type test and a cast in one expression:

if (pet is Dog dog) { dog.Fetch(); }

This is concise and avoids a separate null check. Pattern matching also works in switch expressions, which is useful when handling multiple derived types.

Type Checking and Pattern Matching

The is operator checks the runtime type without casting. In modern C#, you can use the type pattern to both test and assign:

if (pet is Dog) { Console.WriteLine("It's a dog"); }

The pattern pet is Dog dog introduces a new variable dog that is safely cast. This is often the cleanest way to branch on the actual type.

Switch expressions can also use type patterns:

string sound = pet switch { Dog d => d.Speak(), Cat c => c.Speak(), _ => "Unknown" };

This keeps the logic readable and avoids a chain of if-else checks. Pattern matching evaluates the runtime type and does not affect the static type of the original variable.

When to Use a Base Type Reference

Using a base type reference is appropriate when you need to operate on a collection of related objects without knowing their exact derived type. A list of Animal objects can hold dogs, cats, and birds, and you can call common methods on all of them.

This is also the foundation of dependency injection and many design patterns. A method that accepts a base type is more flexible than one that accepts a specific derived type because it can work with any future derived class that follows the same contract.

However, if you constantly cast or pattern match to access derived-specific behavior, the design may be flawed. Consider whether the base type exposes the right abstraction. Sometimes an interface is a better choice than a base class, especially when derived types do not share implementation code.

Runtime Cost and Maintainability Tradeoffs

Virtual dispatch adds a small runtime cost compared to non-virtual calls. In most business applications, this is not a concern. But in performance-sensitive code, such as game engines or high-frequency trading systems, you may want to avoid virtual calls in hot paths. One alternative is to use generics with constraints, which can sometimes enable inlining and avoid virtual dispatch.

Type checking and casting also have costs. The is and as operators perform runtime type checks, which are fast but not free. If you have a large collection and repeatedly test types, the overhead can add up. In such cases, consider restructuring the data to avoid type checks altogether, for example by using separate collections per type.

From a maintainability perspective, base type references make code more extensible because you can add new derived types without changing existing methods. The tradeoff is that you lose compile-time access to derived members, so you must rely on virtual methods and pattern matching. This can make the code harder to follow if the type hierarchy is deep or the patterns are overused.

Alternatives: Generics and Interfaces

Generics provide a way to work with types without losing type information. A generic method can accept any type that implements a specific interface:

void Process<T>(T item) where T : IAnimal { item.Speak(); }

Here, the compiler knows the exact type T at the call site, so you can call interface methods without casting. This can be more efficient and type-safe than using a base type reference.

Interfaces are often preferred over base classes when you want to define a contract without forcing an inheritance hierarchy. A class can implement multiple interfaces, but it can inherit from only one base class. Using an interface as the reference type gives you similar polymorphic behavior with more flexibility.

The choice between a base type reference, an interface, and generics depends on the design. If you need to share implementation code, a base class is useful. If you only need a contract, an interface is lighter. If you need to preserve the exact type for performance or type safety, generics are the way to go.

c# base type reference derived object: Practical Usage and C | RYUSLOG DEV