Click here to Skip to main content
15,887,376 members
Articles / Programming Languages / C# 7.0

Abolishing Switch-Case Statement and Pattern Matching in C# 7.0

Rate me:
Please Sign up or sign in to vote.
3.86/5 (8 votes)
18 Jun 2017CPOL6 min read 12.9K   1   5
Moving from switch-case to object oriented code, and pattern matching
This post discusses some problems associated with using switch-case statement and pattern matching in C#7.

There are many arguments on the web regarding the switch-case statement. It seems that half the programmers think that switch-case statement is actually an anti-pattern, and the other half claims 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 otherwise and I am belonging fully to the first group.

These statements in these so-called “simple situations” often get out of hand, and what we usually end up with are 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 Open-Close Principle. This is especially the case in enterprise systems and this kind of thinking leads to maintenance hell.

Another problem of 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 an 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:

C#
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;
}

Console.ReadLine();

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, i.e., mapping “Item1” to action1, “Item2” to action2, etc.

switch

As you can see, 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?

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.

C#
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:

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

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. 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:

C#
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:

C#
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.

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:

C#
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:

C#
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, 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 like. It is more resilient to changes and its behavior is separated from the data. Data no longer controls 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.

Another complaint that I usually get is that this code still doesn’t fully satisfy Open Close Principle, meaning that if new entity type needs to be added, we need to change 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 to, but many times we cannot quite get there. In my opinion, Open Close Principle is the most unreachable of all SOLID principles.

Pattern Matching

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

What is pattern matching after all? To better explain this, I’ll use 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:

F#
type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float

This means is that we have defined type Shape, that can be either Rectangle or 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 that will get the height of the shape. That would look something like this:

F#
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 I was very happy when I saw that it will be a part of the new C#.

So how would this code look like in C# before improvements?

C#
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);
    }
}

What this feature is giving 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.

C#
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 using of when clause.

C#
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;
    }
}

Conclusion

To be honest, I am not very impressed with possibilities that pattern matching in C# 7.0 provided us with. What makes this concept so powerful in F# are basically discriminated unions, and without them, pattern matching in C# is pretty limited. Also, I haven’t done any real world project using this C# version, so I can not really be the judge of it. My impression is that, switch-case statements got a little bit charged up, and things can look little bit cleaner using them, but that they are still sort of anti-pattern. In the end, I didn’t get convinced that these statements have a valid use case and that they should be used.

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

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Vega IT Sourcing
Serbia Serbia
Read more at my blog: https://rubikscode.net

Comments and Discussions

 
QuestionElegance does not make for readability IMHO Pin
Member 1236439019-Jun-17 22:45
Member 1236439019-Jun-17 22:45 
QuestionDiscriminated Unions (kinda) Pin
John B Oliver19-Jun-17 13:07
John B Oliver19-Jun-17 13:07 
AnswerRe: Discriminated Unions (kinda) Pin
Nikola M. Živković19-Jun-17 22:10
Nikola M. Živković19-Jun-17 22:10 
GeneralMaybe it's to simple Pin
User 584223719-Jun-17 5:52
User 584223719-Jun-17 5:52 
GeneralRe: Maybe it's to simple Pin
Nikola M. Živković19-Jun-17 22:16
Nikola M. Živković19-Jun-17 22:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.