C# Type Switching Patterns: Evolution from Dictionary Delegates to Pattern Matching

Nov 21, 2025 · Programming · 9 views · 7.8

Keywords: C# | Type Switching | Pattern Matching | Dictionary Delegates | Switch Statement

Abstract: This article provides an in-depth exploration of various approaches for conditional branching based on object types in C#. It focuses on the classic dictionary-delegate pattern used before C# 7.0 to simulate type switching, and details how C# 7.0's pattern matching feature fundamentally addresses this challenge. Through comparative analysis of implementation approaches across different versions, it demonstrates the evolution from cumbersome to elegant code solutions, covering core concepts like type patterns and declaration patterns to provide developers with comprehensive type-driven programming solutions.

Historical Context of Type Switching Problem

Prior to C# 7.0, the language lacked native support for switch branching based on object types. Developers frequently encountered scenarios requiring different logic execution depending on the actual type of input objects. Traditional solutions suffered from various limitations—either verbose code, poor performance, or lack of type safety.

Dictionary Delegate Pattern: Elegant Pre-C# 7.0 Solution

Before pattern matching emerged, using dictionaries combined with delegates represented one of the most elegant and efficient solutions. This pattern's core concept establishes mapping relationships between types and corresponding processing logic:

var typeSwitch = new Dictionary<Type, Action>
{
    { typeof(Type1), () => 
    {
        // Processing logic for Type1
        Console.WriteLine("Processing Type1 object");
    }},
    { typeof(Type2), () => 
    {
        // Processing logic for Type2
        Console.WriteLine("Processing Type2 object");
    }},
    { typeof(Type3), () => 
    {
        // Processing logic for Type3
        Console.WriteLine("Processing Type3 object");
    }
};

// Usage example
object myObject = new Type1();
typeSwitch[myObject.GetType()]();

Advantages of this approach include:

However, this method also has limitations: inability to implement traditional switch statement fall-through mechanisms and awkwardness when handling complex conditional branching.

Limitations of Other Traditional Solutions

Beyond the dictionary delegate pattern, developers experimented with various alternative approaches:

String Type Name Comparison

switch(myObj.GetType().ToString())
{
    case "Namespace.Type1":
        // Processing logic
        break;
    case "Namespace.Type2":
        // Processing logic
        break;
    // ...more cases
}

Problems with this method:

Sequential if-else Checks

if (myObj is Type1)
{
    var t1 = (Type1)myObj;
    // Processing logic
}
else if (myObj is Type2)
{
    var t2 = (Type2)myObj;
    // Processing logic
}
else if (myObj is Type3)
{
    var t3 = (Type3)myObj;
    // Processing logic
}

Disadvantages of this approach:

Revolutionary Improvements with C# 7.0 Pattern Matching

C# 7.0's pattern matching feature fundamentally solves the type switching problem. The new syntax allows direct type-based pattern matching within switch statements:

switch (myObj)
{
    case Type1 t1:
        // Direct use of t1, no explicit casting needed
        Console.WriteLine(t1.Type1Property);
        break;
    case Type2 t2:
        // Direct use of t2
        Console.WriteLine(t2.Type2Property);
        break;
    case Type3 t3:
        // Direct use of t3
        Console.WriteLine(t3.Type3Property);
        break;
    case null:
        throw new ArgumentNullException(nameof(myObj));
    default:
        throw new ArgumentException($"Unsupported type: {myObj.GetType()}");
}

Core Advantages of Pattern Matching

Advanced Pattern Matching Features

Subsequent C# versions beyond 7.0 further enhanced pattern matching capabilities, introducing more powerful patterns:

Type Pattern

public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
    Car => 2.00m,
    Truck => 7.50m,
    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException("Unknown vehicle type", nameof(vehicle))
};

Declaration Pattern

object greeting = "Hello, World!";
if (greeting is string message)
{
    Console.WriteLine(message.ToLower()); // Output: hello, world!
}

Property Pattern

static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

Comparative Analysis: Pattern Matching vs Dictionary Delegates

<table> <thead> <tr> <th>Feature</th> <th>Dictionary Delegate Pattern</th> <th>Pattern Matching</th> </tr> </thead> <tbody> <tr> <td>Syntax Conciseness</td> <td>Medium</td> <td>Excellent</td> </tr> <tr> <td>Type Safety</td> <td>Good</td> <td>Excellent</td> </tr> <tr> <td>Performance</td> <td>Excellent (O(1))</td> <td>Good</td> </tr> <tr> <td>Extensibility</td> <td>Excellent</td> <td>Excellent</td> </tr> <tr> <td>Code Readability</td> <td>Good</td> <td>Excellent</td> </tr> <tr> <td>IDE Support</td> <td>Good</td> <td>Excellent</td> </tr> </tbody>

Practical Application Scenarios

Data Processing Pipeline

public object ProcessData(object input) => input switch
{
    int number => ProcessNumber(number),
    string text => ProcessText(text),
    DateTime date => ProcessDate(date),
    IEnumerable<object> collection => ProcessCollection(collection),
    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException($"Unsupported data type: {input.GetType()}")
};

Polymorphic Object Handling

public interface IShape { }
public record Circle(double Radius) : IShape;
public record Rectangle(double Width, double Height) : IShape;

public double CalculateArea(IShape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    _ => throw new ArgumentException("Unknown shape type")
};

Migration Strategy and Best Practices

Recommendations for migrating from traditional type switching to pattern matching in existing projects:

  1. Gradual Migration: Prioritize pattern matching in new code, gradually refactor old code
  2. Compatibility Maintenance: Ensure .NET version supports pattern matching (C# 7.0+)
  3. Team Training: Ensure development team understands advantages and usage of new patterns
  4. Code Review: Encourage pattern matching adoption over traditional approaches during reviews

Conclusion

The evolution of C# type switching solutions has progressed from cumbersome to elegant approaches. The dictionary delegate pattern provided an excellent solution before C# 7.0, while pattern matching introduction fundamentally transformed programming paradigms in this domain. Modern C# development should prioritize pattern matching for type-based conditional branching, resulting in cleaner, safer code with better development experience and maintainability.

For projects requiring support for older C# versions, the dictionary delegate pattern remains a reliable choice. However, as the .NET ecosystem evolves, migrating to modern C# versions supporting pattern matching should be the long-term goal for most projects.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.