How do you run different code depending on an object’s type? In older C#, that simple task often meant long chains of if or else statements using GetType() or is. It worked, but the result was messy and harder to maintain.
Switching on type means controlling program flow based on an object’s runtime type. When working with interfaces or class hierarchies, this can keep logic clear and predictable. Modern C# now makes this cleaner through pattern matching in switch statements, removing the need for manual checks or unsafe casting.
In this guide, we’ll examine how the problem was previously handled and how the switch statement simplifies it.
The Old Approach: Manual Type Checking
Before switch statements supported type comparisons, developers had to manually identify object types. The usual method involved calling GetType() or using the is keyword inside long conditional blocks. This approach worked, but as the number of possible types increased, the logic quickly became repetitive and more difficult to manage.
Let’s look at a simple example. Suppose there’s an interface called IVehicle with two classes that implement it: Car and Truck. A program might need to react differently depending on the type of vehicle it receives.
public interface IVehicle
{
void Drive();
}
public class Car : IVehicle
{
public void Drive() => Console.WriteLine("Driving a car");
}
public class Truck : IVehicle
{
public void Drive() => Console.WriteLine("Driving a truck");
}
public static void DescribeVehicle(IVehicle vehicle)
{
if (vehicle == null)
{
Console.WriteLine("No vehicle provided");
}
else if (vehicle.GetType() == typeof(Car))
{
Console.WriteLine("It's a car");
}
else if (vehicle.GetType() == typeof(Truck))
{
Console.WriteLine("It's a truck");
}
else
{
Console.WriteLine("Unknown vehicle type");
}
}
While this code works, it has clear problems. Each time a new vehicle type is added, the conditional block grows longer. The type checks are repetitive and not type-safe, since every branch must manually cast or identify the object.
The Modern Approach: Type Patterns in Switch
Modern C# made type checking inside a switch statement much simpler through pattern matching. This means you can now test an object’s type and safely use it within the same case, all in one clean construct. It eliminates the need for lengthy chains of if-else statements and makes the code easier to extend and maintain.
Let’s rewrite our earlier example using a switch statement that supports type patterns.
public static void DescribeVehicle(IVehicle vehicle)
{
switch (vehicle)
{
case Car car:
Console.WriteLine("It's a car");
break;
case Truck truck:
Console.WriteLine("It's a truck");
break;
case null:
Console.WriteLine("No vehicle provided");
break;
default:
Console.WriteLine("Unknown vehicle type");
break;
}
}
Here, the switch statement checks the runtime type of vehicle. When it matches a Car, the object is automatically cast and available as a car within that case. The same logic applies to Truck. There is no need for explicit type comparisons or manual casting. The null and default cases ensure all possibilities are handled safely.
This version reads clearly and scales better. Adding a new type only means adding another case block. Pattern matching transforms what was once a repetitive set of conditions into structured, readable logic that anyone maintaining the code can easily follow.
Expanded Pattern Matching
Type patterns are only one part of a broader idea known as pattern matching. Modern switch statements can now test values, ranges, and even conditions inside a single clean structure. This allows developers to handle more than just object types while keeping logic easy to read.
Let’s look at a simple example that checks a numeric value and reacts based on its range.
public static void DescribeSpeed(int speed)
{
switch (speed)
{
case < 0:
Console.WriteLine("Invalid speed");
break;
case 0:
Console.WriteLine("The vehicle is stationary");
break;
case > 0 and <= 60:
Console.WriteLine("Normal driving speed");
break;
case > 60 and <= 120:
Console.WriteLine("High speed");
break;
default:
Console.WriteLine("Extremely high speed");
break;
}
}
This example demonstrates that switch statements can now include relational patterns like < or >, and even combine them with logical patterns such as “and” and “or“. Each case directly describes the condition, rather than relying on nested comparisons.
You can also use conditions known as guards with the when keyword for extra control.
case > 0 when speed % 10 == 0:
Console.WriteLine("Speed is a clean multiple of ten");
break;
Pattern matching makes switch statements flexible enough to handle both types and conditions with the same clarity. It reduces repetition and keeps logic grouped in one place, which makes it easier for others to follow and maintain your code.
Wrapping Up
Switching on the type used to be one of those everyday problems that required too much effort for a simple goal. With pattern matching, the switch statement has turned into a tool that’s both powerful and readable. You can now check types, match conditions, and even handle special cases all in one structure.
The key takeaway is to use pattern matching when your logic depends on a value or type, not when the behavior naturally belongs inside the class itself. For example, if every vehicle drives differently, that logic should stay in the class rather than in the switch. But if you are deciding how to display or process objects of different types, a switch with type patterns is ideal.
Keep your cases focused, cover all possibilities, and let pattern matching do the work of testing and casting for you. It makes your code safer, easier to read, and ready for whatever new types your program might grow into.