Casting and Type Checking
Vaibhav • September 10, 2025
In the previous article, we explored how polymorphism allows objects to behave differently based on their runtime type. Now we turn to a closely related topic: casting and type checking. These are essential tools for working with polymorphic objects, especially when you need to convert between types or verify an object’s compatibility before performing operations. In this article, we’ll walk through the different kinds of casting in C#, how to check types safely, and how to avoid common pitfalls when working with inheritance and interfaces.
Why casting matters in polymorphism
When you work with polymorphic objects - for example, treating a Dog
as an
Animal
- you often need to cast between types. Casting allows you to access
members that are specific to a derived type, or convert between interfaces and concrete classes. But casting
must be done carefully, because not all conversions are valid at runtime.
class Animal
{
public void Speak()
{
Console.WriteLine("Animal sound");
}
}
class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Woof!");
}
}
Suppose you have an Animal
reference that actually points to a Dog
object. You can cast it back to Dog
to
access Bark()
:
Animal pet = new Dog();
pet.Speak(); // OK
Dog dog = (Dog)pet;
dog.Bark(); // OK
This cast works because pet
actually refers to a Dog
. But if it referred to a different subclass, the cast would fail at runtime.
Types of casting in C#
C# supports several ways to cast between types. Each has its own behavior and use case:
1. Explicit casting (unsafe)
This uses the cast operator (Type)
. It throws an exception if the cast is
invalid.
Animal pet = new Animal();
Dog dog = (Dog)pet; // Runtime error: InvalidCastException
Use this only when you’re sure the object is of the target type. Otherwise, prefer safer alternatives.
2. Safe casting with as
The as
operator returns null
if the cast
fails, instead of throwing an exception.
Animal pet = new Animal();
Dog dog = pet as Dog;
if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("Not a dog");
}
This is safer and more readable, especially when working with interfaces or unknown types.
3. Type checking with is
The is
operator checks whether an object is of a given type. It returns a
boolean.
if (pet is Dog)
{
((Dog)pet).Bark();
}
This pattern is common when you need to verify the type before casting. It avoids runtime errors.
Pattern matching with is
Modern C# supports pattern matching with is
, allowing you to combine type
checking and casting in one step.
if (pet is Dog dog)
{
dog.Bark();
}
This syntax checks if pet
is a Dog
, and if so,
declares a new variable dog
with the correct type. It’s concise and safe.
Use pattern matching with is
whenever possible. It’s safer
than explicit casting and more readable than separate checks.
Casting with interfaces
Casting is especially useful when working with interfaces. You might receive an object as an interface type and need to access its concrete members.
interface IShape
{
double Area();
}
class Circle : IShape
{
public double Radius;
public Circle(double radius) { Radius = radius; }
public double Area() => Math.PI * Radius * Radius;
public void Draw()
{
Console.WriteLine("Drawing circle...");
}
}
Suppose you have an IShape
reference:
IShape shape = new Circle(5);
Console.WriteLine(shape.Area()); // OK
Circle circle = shape as Circle;
circle?.Draw(); // Safe cast
This allows you to access Draw()
, which is not part of the interface. But be
careful - not all IShape
objects are Circle
s.
Casting and inheritance hierarchies
Inheritance hierarchies often involve multiple levels. You can cast up and down the hierarchy, but only when the actual object supports it.
class Vehicle
{
public void Start() => Console.WriteLine("Starting vehicle");
}
class Car : Vehicle
{
public void Drive() => Console.WriteLine("Driving car");
}
class SportsCar : Car
{
public void Turbo() => Console.WriteLine("Turbo boost!");
}
You can cast a SportsCar
to Car
or Vehicle
safely:
SportsCar sc = new SportsCar();
Car c = sc; // Upcast
Vehicle v = sc; // Upcast
But downcasting requires caution:
Vehicle v2 = new Car();
SportsCar sc2 = v2 as SportsCar;
if (sc2 != null)
{
sc2.Turbo();
}
else
{
Console.WriteLine("Not a sports car");
}
This cast fails because v2
is actually a Car
,
not a SportsCar
. Always check before casting down.
Casting and collections
When working with collections of base types or interfaces, you may need to cast individual items to access specific behavior.
List shapes = new List
{
new Circle(2),
new Circle(3)
};
foreach (IShape shape in shapes)
{
if (shape is Circle c)
{
c.Draw();
}
}
This pattern is common in UI frameworks, game engines, and data processing pipelines. It allows you to treat items generically while still accessing specific behavior when needed.
Avoiding casting altogether
In many cases, you can avoid casting by designing your interfaces and base classes carefully. If a method is needed by all consumers, include it in the interface. If a property is shared across types, define it in the base class.
interface IShape
{
double Area();
void Draw(); // Add to interface
}
Now you don’t need to cast to access Draw()
. This simplifies your code and
reduces the risk of runtime errors.
Design interfaces to include all necessary behavior. Avoid casting unless you truly need access to type-specific members.
Common mistakes and how to avoid them
Casting is powerful, but it’s easy to misuse. Here are some common mistakes:
1. Casting without checking: Always verify the type before casting. Use is
or as
to avoid exceptions.
2. Assuming interface types are always a specific class: Interfaces can be implemented by many classes. Don’t assume a specific implementation.
3. Overusing casting: If you find yourself casting frequently, reconsider your design. You may need better interfaces or base classes.
4. Ignoring null results from as
: Always check for null
after using as
. Otherwise, you risk NullReferenceException
.
C# also supports dynamic
typing, which defers type checking
to runtime. It’s powerful but should be used sparingly in statically typed codebases.
Summary
Casting and type checking are essential tools for working with polymorphic objects in C#. You’ve learned how to
use explicit casting, safe casting with as
, type checking with is
, and pattern matching. You’ve seen how to cast between interfaces and classes,
navigate inheritance hierarchies, and avoid common mistakes. By mastering these techniques, you’ll write safer,
more flexible code that handles dynamic behavior with confidence.
In the next article, we’ll explore the Object Class - the root of all types in C#, and how its
methods like ToString()
, Equals()
, and GetHashCode()
shape object behavior across the language.