There are many arguments on the web regarding the switch-case statement. It seems that half of the programmers think that the switch-case statement is actually an anti-pattern, and the other half claim there are in fact use cases for this concept. Usually, the second group tries to prove a point that in some simple situations it is alright to use switch-case, like some really simple checking. My experience tells me other otherwise and I am belonging fully to the first group.

ML.NET Full-Stack: Complete Guide to Machine Learning for .NET Developers

From the basics of machine learning to more complex topics like neural networks, object detection and NLP, this course will guide you into becoming ML.NET superhero.

1. Problems with Switch-Case Statement

These statements in these so-called “simple situations” often get out of hand, and we usually end up with large unreadable chunks of code. Not to mention that if the requirements change (and we know that they do), the statement itself has to be modified, thus breaking the Open-Close Principle. This is especially the case in enterprise systems and this kind of thinking leads to maintenance hell.

Another problem with switch-case statements (just like if-else statements) is that they combine data and behavior, which is something that reminds us of procedural programming. What does this mean and why is it bad? Well, data in its essence is information that doesn’t contain behavior.  Treating data as behavior and using it for controlling the workflow of your application is what creates mentioned maintenance hell. For example, take a look at this code:

string data = "Item1";

var action1 = new Action(() => { Console.Write("This is one!"); });
var action2 = new Action(() => { Console.Write("This is two!"); });
var action3 = new Action(() => { Console.Write("This is three!"); });

switch (data)
{
    case "Item1":
        action1();
        break;
    case "Item2":
        action2();
        break;
    default:
        action3();
        break;
}

Data in this code is variable named data, but also data is the string that is printed on the console. The behavior is the action of printing information on the console, but also the behavior is mapping set of information from the dataset to some action, ie. mapping “Item1” to action1, “Item2” to action2, etc.

Programming Visual

As you can see that data is something that can be changed and behavior is something that shouldn’t be changed, and mixing them together is the cause of the problem.

So, how to get rid of switch-case statements?

2. From Switch-Case to Object-Oriented Code

Recently, I was working on the code that relied on the switch-case statement. So, I created the next example based on that real-world problem and on the way I refactored it.

public class Entity
{
    public string Type { get; set; }

    public int GetNewValueBasedOnType(int newValue)
    {
        int returnValue;
        switch (Type)
        {
            case "Type0":
                returnValue = newValue;
                break;
            case "Type1":
                returnValue = newValue * 2;
                break;
            case "Type2":
                returnValue = newValue * 3;
                break;
        }

        return newValue;
    }
}

Here we are having entity class with the property Type. This property defines how the function GetNewValueBasedOnType will calculate its result. This class is used in this manner:

var entity = new Entity() { Type = "Type1" };
var value = entity.GetNewValueBasedOnType(6);
Coding Visual

In order to modify this example into the proper object-oriented code, we need to change quite a few things. The first thing we can notice is that even though we have multiple Entity types, they are still just Entity types. A donut is a donut, no matter what flavor it is. This means that we can create a class for each entity type, and all those classes should implement one abstract class. Also, we can define an enumeration for the entity type. That looks like this:

public enum EntityType
{
    Type0 = 0,
    Type1 = 1,
    Type2 = 2
}

public abstract class Entity
{
    public abstract int GetNewValue(int newValue);
}

Concrete implementations of Entity class look like this:

public class Type0Entity : Entity
{
    public override int GetNewValue(int newValue)
    {
        return newValue;
    }
}

public class Type1Entity : Entity
{
    public override int GetNewValue(int newValue)
    {
        return 2*newValue;
    }
}

public class Type2Entity : Entity
{
    public override int GetNewValue(int newValue)
    {
        return 3*newValue;
    }
}

That is much better. Now we are closer to real object-oriented implementation, not just some fancy procedural implementation. We used all those nice concepts that object-oriented programming provides us with, like abstraction and inheritance.

Data Visual

Still, there is a question how to use these classes. It seems that we didn’t remove the switch-case statement, we just moved it from Entity class to the place where we will create an object of concrete Entity implementations. Basically, we still need to determine which class we need to instantiate. At this point we can make Entity Factory:

public class EntityFactory
{
    private Dictionary<EntityType, Func<entity>> _entityTypeMapper;

    public EntityFactory()
    {
        _entityTypeMapper = new Dictionary<entitytype, func<entity="">>();
        _entityTypeMapper.Add(EntityType.Type0, () => { return new Type0Entity(); });
        _entityTypeMapper.Add(EntityType.Type1, () => { return new Type1Entity(); });
        _entityTypeMapper.Add(EntityType.Type2, () => { return new Type2Entity(); });
    }

    public Entity GetEntityBasedOnType(EntityType entityType)
    {
        return _entityTypeMapper[entityType]();
    }
}

Now, we can use this code like this:

try
{
    Console.WriteLine("Enter entity type:");
    var entytyType = (EntityType)Enum.Parse(typeof(EntityType), Console.ReadLine(), true);

    Console.WriteLine("Enter new value:");
    var modificationValue = Convert.ToInt32(Console.ReadLine());

    var entityFactory = new EntityFactory();
    var entity = entityFactory.GetEntityBasedOnType(entytyType);
    var result = entity.GetNewValue(modificationValue);

    Console.WriteLine(result);
    Console.ReadLine();
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Many times I showed this code to someone, and I would get a comment that this is too complicated. But, the thing is that this is the way that real object-oriented code should look. It is more resilient to changes and its behavior is separated from the data. Data no longer control the workflow. We can see that entity type and entered value are changeable, but that they are not intervened with behavior. By doing that we increased the maintainability of the code. The next evolutionary step for this code would be the use of the Strategy Pattern.

Another complaint that I usually get is that this code still doesn’t fully satisfy the Open-Close Principle, meaning that if a new entity type needs to be added, we need to change the Entity Factory class itself. This is a very good complaint, and it is right on point. Nevertheless, we shouldn’t forget that code that fulfills SOLID principles is something we should always strive for, but many times we can not quite get there. In my opinion, the Open-Close Principle is the most unreachable of all SOLID principles.

 3. Pattern Matching in C# – Evolution

You might ask yourself why is this chapter here. We were talking about the switch-case statement, so what does pattern matching has to do with this? Well, remember when I said that I think that switch-case shouldn’t be used? The thing is that a couple of years ago, C# 7 introduced feature pattern matching. This feature is already well established in functional programming languages, like F#, and it heavily relies on the switch-case statement, so let’s take a moment and go a bit deeper into this and see if this could change my mind.

What is pattern matching after all? To better explain this I’ll use the F# example. F# has this mighty concept called discriminated union. Using this concept we can define a variable, that can have multiple types, meaning not only value is changeable but also the type is changeable too. Wait, what? Here is an example:

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
Programming Visual

This means that we have a defined type of Shape, which can be either a Rectangle or a Circle. Variable of type shape could be either of these two types. Like Schrodinger’s cat, we will not know the type of that variable until we take a closer look. Now, let’s say we want to write a function tachat will get the height of the shape. That would look like something like this:

let getShapeHeight shape =
    match shape with
    | Rectangle(width = h) -> h
    | Circle(radius = r) -> 2. * r

What happened here is that we checked the type of the variable shape, and returned Rectangle width if the shape is of type Rectangle, or returned two times radius value if the shape is of type Circle. We have opened the box and collapsed a wavefunction.

Cat

Although this concept seems unnecessarily complicated at first, in practice it is both elegant and powerful. Controlling the workflow gives us so many possibilities, so back in the day, I was very happy when I saw that it will be a part of C#. So how this code would look in C# before improvements?

var shapes = new Dictionary<string, object>
{
    { "FirstShape", new Rectangle(1, 1) },
    { "SecondShape", new Circle(6) }
};

foreach(var shape in shapes)
{
    if (shape.Value is Rectangle)
    {
        var height = ((Rectangle)shape.Value).Height;
        Console.WriteLine(height);
    }
    else if (shape.Value is Circle)
    {
        var height = ((Circle)shape.Value).Radius * 2;
        Console.WriteLine(height);
    }
}

3.1 Pattern Matching in C# 7

What this feature gives us in C# is the ability to simplify this syntax. It is giving a little bit more usability to the switch statement too, meaning that now we can switch by the type of the variable.

foreach (var shape in shapes)
{
    switch(shape.Value)
    {
        case Rectangle r:
            Console.WriteLine(r.Height);
            break;
        case Circle c:
            Console.WriteLine(2 * c.Radius);
            break;
    }
}

Another thing that can be done now is to use the when clause.

foreach (var shape in shapes)
{
    switch(shape.Value)
    {
        case Rectangle r:
            Console.WriteLine(r.Height);
            break;
        case Circle c when c.Radius > 10:
            Console.WriteLine(2 * c.Radius);
            break;
    }
}

This means that we can check for specific properties of the object in the case condition. Now, this feature really improved over the years with the newer versions of C#, so this was just a start for pattern matching. Let’s take a look.

Data Visual

3.2 Pattern Matching in C# 8

With C#8 we got switch-case expressions. This feature aimed to further improve switch-case usability and worked really well with pattern matching. I mean, really well. these two features together give us the ability to write code like this:

var message = shape switch 
{
	Circle cir => "This is a circle with area {cir.Area}!",
	Rectange rec when rec.height==rec.width => "This is a square!",
	{ Area : 111 } => "Area is 111",
	_ => "This is a shape!"
}

So, with this approach, we have more control. We can check the type, the value of properties of a specific type, the value of the properties of the base type, and so on. However, note that we can not say {Area > 111}, we have to be really specific about the value. So, this was a big step, but it still wasn’t quite there.

3.3 Pattern Matching in C# 9

C# 9 improved exactly the problem I mentioned in the previous section. However, that was not the only improvement. Guys at Microsoft listen to the feedback from the community and improved the pattern matching, so we could do this:

var message = shape switch 
{
	Circle cir => "This is a circle with area {cir.Area}!",
	Rectangle rec when rec.height == rec.width => "This is a square!",
	Circle cir { Area : > 111 and < 222} => "This is a Circle with specific area value",
	{ Area == 111 } => "Area is 111",
	_ => "This is a shape!"
}
    
var areaMessage = shape.Area switch
{
    > 111 and < 222 => "This is a specific area",
    _ => "Nothing specific about the area"
}

This is where a lot of things clicked. You could use operators like and  and not to match specific patterns. At least in my eyes, this was a big leap for pattern matching and for switch-case.

Programming Visual

3.4 Pattern Matching in C# 10

In C#10, Microsoft continued to improve pattern matching, giving us the ability to check objects within the object. For example, if we extended Shape class like this:

public abstract class Shape
{
	public abstract double Area { get; }
  
	public Shape ShapeWithinShape { get; set; }
}

So, we added a property that represents shape within the shape. This can be used with pattern matching like this:

if (shape is Rectangle { ShapeWithinShape.Area == 111 })
{
	// Do some amaizing stuf
}

3.5 Pattern Matching in C# 11

There are a couple of C# 11 improvements when it comes to pattern matching. For example, in C# 11 you can match Span and ReadOnlySpan with the constant string. Also, working on data science projects is getting easier with .NET technologies because there is a feature for Lists Pattern matching. It is interesting that a similar feature was added in the previous version of Python. Find out more about these C# 11 features here.

Conclusion

To be honest, nowadays I can see switch-case statements crawling their way back into my code. Of course, it is in the form of expressions and pattern matching. I am still firmly against using switch-case out of the box. However, seeing pattern matching evolve over years got my mind swarming with ideas.

What I haven’t covered in this post is the Strategy pattern, which also can be a good choice in some situations, if you want to remove switch-case from your code.

ML.NET Full-Stack: Complete Guide to Machine Learning for .NET Developers

From the basics of machine learning to more complex topics like neural networks, object detection and NLP, this course will guide you into becoming ML.NET superhero.

Discover more from Rubix Code

Subscribe now to keep reading and get access to the full archive.

Continue reading