Keywords: C# | .NET | type switching | pattern matching | if/else
Abstract: This article examines methods for conditionally branching based on object types in the C# programming language. From traditional if/else chains to the pattern-matching switch statement introduced in C# 7, and custom solutions, it provides comprehensive analysis and code examples to help developers optimize code structure and maintainability.
In C# development, when handling objects of different types, developers often use if/else statements for type checks, such as if(obj is WebControl). As code complexity increases, this approach becomes verbose and hard to maintain, prompting a search for more concise alternatives.
Type Pattern Matching in C# 7 and Later
Starting with C# 7, the language introduced pattern matching, allowing type-based branching in switch statements. This offers flexibility similar to virtual method dispatch while maintaining code readability. For example, the following code demonstrates handling shape objects using type pattern matching:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle r when (r.Length == r.Height):
WriteLine($"{r.Length} x {r.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}In this example, case Circle c: directly matches the type and binds it to variable c, allowing access to its properties. Additionally, the when clause supports more complex conditions. This syntax avoids explicit type casting, enhancing code expressiveness.
Solutions Prior to C# 7
Before C# 7, the language lacked built-in syntax for type switching. According to Peter Hall's blog, type switching seems simple but can introduce ambiguity when dealing with inheritance and interfaces. For instance, consider class D : C, I {}; in a switch typeof(e), if e is of type D, matching C or I might lead to indeterminate behavior, so C# designers initially avoided implementing this feature.
To address this limitation, developers commonly used custom classes to simulate type switching. For example, the TypeSwitch class uses a dictionary to map types to delegates:
public class TypeSwitch
{
Dictionary<Type, Action<object>> matches = new Dictionary<Type, Action<object>>();
public TypeSwitch Case<T>(Action<T> action) { matches.Add(typeof(T), (x) => action((T)x)); return this; }
public void Switch(object x) { matches[x.GetType()](x); }
}Usage example: var ts = new TypeSwitch().Case((int x) => Console.WriteLine("int")).Switch(42);. This method relies on GetType() for exact type matching but does not handle inheritance hierarchies and requires manual management.
Comparison and Recommendations
For new projects or environments upgraded to C# 7 and later, the pattern-matching switch statement is recommended. It is integrated into the language, provides safer and more expressive syntax, and supports pattern combinations like constant and property patterns. For older C# versions, the custom TypeSwitch class offers a flexible but slightly cumbersome alternative, with limitations such as not automatically handling type hierarchies.
In summary, C#'s type-switching capabilities have evolved with the language, and developers should choose appropriate methods based on project needs and C# version. Pattern matching is a key feature for optimizing modern C# code, helping reduce redundancy and improve maintainability.