Upcasting and downcasting are important concepts in C# programming that allow us to convert an object of one type to another type. These concepts are essential to work with polymorphism and object-oriented programming.

In this article, we will explore how upcasting and downcasting work, when to use them, and common pitfalls to avoid. 

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

Before we dive into upcasting and downcasting let’s start by setting up our classes.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Class Structure

Let’s create a class hierarchy of a base class Animal and derived classes Snake and Owl:

public abstract class Animal
{
    public abstract string MakeSound();
}

Here, we have an abstract class Animal with a MakeSound() method.

Let’s create the concrete classes to implement this method:

public class Snake : Animal
{
    public override string MakeSound()
    {
        return "Hiss";
    }

    public string Move()
    {
        return "Slithering...";
    }
}

Snake is a class derived from the Animal class and has its own implementation of the MakeSound() method. Also, we have an additional Move() method specific to the class.

Let’s create another derived class Owl:

public class Owl : Animal
{
    public override string MakeSound()
    {
        return "Hoot";
    }

    public string Fly()
    {
        return "Flying...";
    }
}

Our implementation of the MakeSound() method in the derived Owl class differs from Snake. Aso, we create a Fly() method specific to the class.

Now that we have a base to work upon, let’s begin with upcasting.

Upcasting in C#

Upcasting is a concept in C# that allows us to treat a derived class as its base class.

In other words, upcasting is the process of converting an object of a derived class to an object of its base class. We achieve this through implicit type conversion, where the derived class object is assigned to the base class object.

After the implicit conversion, we can treat the object as if it is an instance of the base class. This means that the properties and methods of the derived class that are not present in the base class will not be accessible.

Upcasting Example

We can use upcasting to create a list of objects that contains different types of derived class objects.

For example, we can create a list of Animal objects that contains a Snake object as well as an Owl object. Since both Snake and Owl inherit from Animal, we can upcast them to Animal and add them to the list:

var animals = new List<Animal>
{
    new Snake(),
    new Owl()
};

Then, we can iterate over the list of Animal objects and call the MakeSound() method on each one, without worrying about their specific types.

This is because both Snake and Owl implement the MakeSound() method, which is defined in the base Animal class:

foreach (var animalObj in animals)
{
    Console.WriteLine("It says: " + animalObj.MakeSound());
}

// It says: Hiss
// It says: Hoot

Thus, we can see how upcasting can be useful when we want to treat different types of objects uniformly.

A common use of upcasting is when we need to pass a derived class object to a function that only accepts the base class object. With upcasting, we can pass the derived class object as a base class object, and the function can perform the necessary operations on it.

Another use case is when a collection of objects of different derived classes needs to be stored in a single collection. In this scenario, we can upcast and store all the objects in the collection using the base class reference.

Advantages of Upcasting

Upcasting simplifies the code and makes it more flexible.

By upcasting objects to a common base class, we can add them to a collection and call methods on them that are defined in the base class without worrying about their specific types. This can help us uniformly treat different types of objects, as we can use the base class to represent several derived classes.

Upcasting can also help us avoid duplicating code for different derived classes and make the code more concise. Additionally, storing objects of different derived classes in a single collection can help us manage the code more efficiently and avoid creating separate collections for each derived class.

Limitations of Upcasting

One of the major limitations of upcasting is that it does not allow access to the derived class’s unique methods or properties.

So, if the derived class contains a method or a property that the base class does not, we cannot access it through the base class reference:

Animal animal = new Snake();
animal.MakeSound();
animal.Move();

Here, once we upcast Snake to Animal, we lose access to the Move() method defined in the Snake class.

The code throws a compiler error “‘Animal’ does not contain a definition for ‘Move’ and no accessible extension method ‘Move’ accepting a first argument of type ‘Animal’ could be found (are you missing a using directive or an assembly reference?)“.

Downcasting in C#

Downcasting is a technique that allows us to treat a base class object as an instance of its derived class.

In C#, downcasting works by explicitly converting a base class reference to a derived class reference using the cast operator:

DerivedClass derivedObject = (DerivedClass) baseObject;

Here, DerivedClass is the name of the derived class, and baseObject is the base class reference that needs to be downcasted.

Let’s understand downcasting with our existing classes:

Animal animal = new Snake();
((Snake)animal).Move();

Here, we create an object of the Snake class and assign it to a variable of the Animal class.

Then, we use the cast operator to convert the Animal class reference to a Snake class reference so that we can call the Move() method.

Example of Downcasting

One common use case of downcasting is event handling.

In C#, event handlers receive an object parameter that contains the sender object that raised the event. Often, the sender object is an instance of a base class, and to access its specific functionality, we can use downcasting. 

To learn more about event handlers, check out our great article Events in C#.

Let’s understand this by creating an AnimalEventArgs class:

public class AnimalEventArgs : EventArgs
{
    public required Animal Animal { get; set; }
}

Here, we create a simple class that derives from EventArgs. It contains a required property of Animal type that allows the event handler to access the object that raised the event.

Let’s see the event handling in action:

Animal animal = new Owl();
animal.MakeSound();

EventHandler<AnimalEventArgs> animalEventHandler = (sender, args) =>
{
    if (args.Animal is Owl owl)
    {
        owl.Fly();
    }
};

var eventArgs = new AnimalEventArgs() { Animal = animal };
animalEventHandler(animal, eventArgs);

Here, we create an event handler animalEventHandler that takes two parameters, the sender object, i.e., the object that raised the event, and an instance of AnimalEventArgs.

Then, we downcast the Animal property of the AnimalEventArgs object to an Owl object and assign it to a variable owl. Here, we are using pattern matching to simplify the process of checking the type and assigning it to a variable in a single operation.

If the downcasting is successful (i.e., owl is indeed of type Owl), we call the Fly() method of the Owl class.

Thus, we can see how downcasting is useful in event handling to access the specific functionality of a derived class even though an object of the base class raised the event.

Advantages of Downcasting

The primary advantage of downcasting is that it allows access to the derived class’s specific methods and properties that are not available in the base class. This can be useful in situations where we need the specific functionality of the derived class.

Another advantage of downcasting is that it can make code more flexible and extensible. When working with large codebases, it’s common to create new classes that derive from existing ones to add new functionality. Downcasting allows us to take advantage of this inheritance hierarchy and easily access the specific functionality of the derived class.

Limitations of Downcasting

While downcasting can be a powerful technique, it has its limitations.

It exposes the implementation details of the derived class and can lead to tight coupling between classes and breaking encapsulation. 

Also, as the downcasting operation is not always type-safe, it is possible to accidentally cast a base class object to an unrelated derived class object, resulting in runtime errors.

Type Checking and Casting

When talking about upcasting and downcasting in C#, it is important to understand type checking and type casting. They are two related concepts that we use to determine and manipulate the type of an object to upcast and downcast them. 

Type checking is the process of determining the type of an object at runtime, while casting is the process of converting an object from one type to another. 

We often use them together to ensure that an object is of the correct type before performing operations on it.

Type Checking

We use the is operator for type checking in C#.

It takes an object and a type as its operands and returns a boolean value indicating whether the object is of the specified type:

Animal animal = new Snake();

if (animal is Snake snake)
{
    snake.Move();
}

Here, we use the is operator is to check if animal variable is an instance of the Snake class. If it is, we downcast animal to a snake variable of Snake type and call the Move() method.

Type Casting

We use the as operator for type casting in C#.

It takes an object and a type as its operands and returns the object cast to the specified type, or null if the object cannot be cast to the specified type:

var obj = new object();
var owlObj = obj as Owl;

if (owlObj != null)
{
    owlObj.Fly();
}

Here, obj is an object that is not an object of type Owl.

We use the as operator to attempt to cast obj to an Owl object. Since obj is not an Owl object, the as operator returns null.  Then, we check if owlObj is null before attempting to call the Fly() method. Thus, we end up not executing the Fly() method.

Also, we can directly cast an object of one type to another without using the as keyword. However, direct casting does not return null if the cast fails; instead, it throws an InvalidCastException.

Thus, direct casting is not a recommended approach.

Differences Between Upcasting and Downcasting

There are some key differences between upcasting and downcasting.

The direction of conversion is opposite in upcasting and downcasting. In upcasting, the conversion is from a derived class to a base class. While in downcasting, the conversion is from a base class to a derived class.

Upcasting is always safe because a derived class object always has all the features of the base class. However, downcasting can be risky because a base class object may not have all the features of the derived class.

Additionally, upcasting is usually done implicitly, while downcasting requires an explicit cast operator or the as keyword.

When choosing between upcasting and downcasting, it’s important to consider the objects involved.

We prefer upcasting when we want to treat a derived class object as a base class object and don’t need to access any specific features of the derived class.

Whereas, downcasting is useful when we want to access specific features of a derived class object that are not available in the base class. However, it’s important to be careful when downcasting to avoid runtime errors.

Best Practices for Upcasting and Downcasting

When working with upcasting and downcasting in C#, it is important to follow some best practices to ensure safe and efficient code. These practices can help us avoid common pitfalls and mistakes that can cause runtime errors and hinder the performance of the application.

One of the best practices when using upcasting and downcasting is to use type-checking before casting. This involves checking the type of an object before performing an upcast or a downcast.

We can use the is operator to check if an object is of a particular type before casting it to that type. This helps to prevent runtime errors that can occur when we cast an object to an unrelated type.

When performing downcasting, it’s a good practice to use the as keyword. The as operator performs a safe downcast by returning null if the object cannot be cast to the specified type.

This helps us avoid runtime errors that can occur when we use an invalid downcast.

Also, it is important to avoid exposing implementation details when using upcasting and downcasting. Exposing implementation details can lead to tight coupling between classes and breaking encapsulation.

Instead, we can define a public interface that exposes only the necessary functionality.

Finally, it is important to prefer interfaces instead of inheritance when possible.

Interfaces provide a more flexible and extensible way to define behavior, and they can be used to achieve polymorphism without the need for upcasting and downcasting.

Conclusion

In this article, we learned about upcasting and downcasting in C#. We looked at their usage, advantages, and limitations. We also learned about the best practices to follow and the pitfalls to avoid. Overall, upcasting and downcasting play a vital role in C# programming and enable us to build robust and flexible applications. Understanding them is crucial for us to write efficient and maintainable code.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!