DEV Community

Cover image for Why switch is better than if-else
edA‑qa mort‑ora‑y
edA‑qa mort‑ora‑y

Posted on • Originally published at mortoray.com

Why switch is better than if-else

In Ben’s post, he questions whether switch statements are cleaner than if-else chains. I contend they are, because they better express the semantics of the code, allow less room for errors, reduce duplication, and potentially improve performance.

Better Semantics

Switch statements express a different meaning than a chain of if-else statements. A switch indicates that you are mapping from an input value to a piece of code.



switch( expr ) {
    case value_0:
        ...

    case value_1:
        ...
}


Enter fullscreen mode Exit fullscreen mode

It’s clear at a glance that we’re attempting to cover all possible values of expr. Somebody reading this code can quickly determine what is supposed to be happening.

This is not as clear with the equivalent if-else chain:



if( expr == value_0) {
    ...
} else if( expr == value_1) {
    ...
}


Enter fullscreen mode Exit fullscreen mode

We aren’t certain here whether we mean to cover all possible values, or only these values in particular.

Many compilers will inform you when a switch statement is missing a condition. In C++ this could be a missing case statement for an enumeration. In Rust, the equivalent match construct covers an even wider range of input, and also disallows missing coverage. This automatic checking by the compiler can prevent common defects. For example, if you add a new value to an enumeration, the compiler can tell you all the locations where you haven’t covered that new case.

Less room for errors and nonsense

One problem of an if-else chain is that it allows any comparison, with any variable. Having no restriction on the form increases the ability to hide errors.



if( expr == value_0 ) {
    ...
} else if( expr == value_1 ) {
    ...
} else if( expr2 == value_2 ) {
    ...
} else if( value_3 == expr ) {
    ...
}


Enter fullscreen mode Exit fullscreen mode

Why is there an expr2 in there? It’s now unclear whether the code intends to cover all values for expr or the conditions are only coincidentally similar.

What about the reversed order of value_3 == expr? This should be corrected in code review, but it’s another possibility of creating confusion.

Refactoring can create this type of problem. An individual may modify the expressions, either fixing an error, or cleaning it up. In a parallel branch another programmer adds a new expression. During merge the two different code forms will come together, resulting in an inconsistent form.

Reduced duplication

Long chains of if-else have unnecessary syntax overhead. Redundancy is one of the principle evils of source code. From the previous example with expr2, we saw that the repeated typing of an expression can’t be ignored. You can’t glance at an if-else chain and assume it’s functioning like a switch, since it may not be. The redundancy adds cognitive load when reading the code.

The duplication could have a performance impact, as well as creating another avenue for errors. I’ve used only expr so far, but what if that expression were a function call?



if( next_obj().get_status() == state_ready ) {
    ...
} else if( next_obj().get_status() == state_pending ) {
    ...
} else {
    ...
}


Enter fullscreen mode Exit fullscreen mode

The first potential error here is the call to next_obj. If the first condition is true, it will evaluate once. If the first condition is false, the next if statement makes another call to the function. Does it return the same value each time, or is it incrementing over a list?

What about get_status()? Is this a cheap or expensive function to call? Maybe it slowly calculates or invokes a database call? Calling it twice doubles whatever the cost is, which may be significant in many cases.

It’s important to store these values in a temporary to avoid both of these problems. This is unfortunately something a lot of coders forget to do, as they quickly copy-paste the first if-else, and then repeat.



state = next_obj().get_status()
if( state == state_ready ) {
    ...
} else if( state == state_pending ) {
    ...
} else {
    ...
}


Enter fullscreen mode Exit fullscreen mode

You could avoid the problem using a switch statement, which only evaluates the expression once.

Better performance

In many cases a switch statement will perform better than an if-else chain. The strict structure makes it easy for an optimizer to reduce the number of comparisons that are made.

This is done by creating a binary tree of the potential options. If your switch statement contains eight cases, only three comparisons are needed to find the right case.



switch( c ) {
    case 0: ...
    case 1: ...
    case 2: ...
    case 3: ...
    case 4: ...
    case 5: ...
    case 6: ...
    case 7: ...
}


Enter fullscreen mode Exit fullscreen mode

An optimizing compiler, or intelligent runtime, can reduce this to a binary search of the numbers.



if( c <4 ) {
    if( c < 2 ) {
        if( c == 0 ) {
            //0
        } else {
            //1
        }
    } else {
        if( c == 3 ) {
            // 3
        } else {
            // 4
        }
    }
} else {
    //repeated for 4...7
}


Enter fullscreen mode Exit fullscreen mode

A clever optimizer might recognize an if-else series the same way. But the potential for minor variations in the statements reduces this possibility. For example, a function call, hidden assignment, or use of an alternate variable, would all prevent this optimization.

By using a semantically significant high-level form, you give the optimizer more options to improve your code.

Language problems

Switch statements aren’t without their problems, however. In particular, the C and C++ form that requires an explicit break statement is problematic. Though, it also allows multiple cases to be packed together.

I like Python a lot, though am upset that it doesn’t have a switch statement. While maps and function dispatching cover several cases, it does not cover all of them.

Rust has a much better match statement. It retains the high-level semantics of a switch statement, but adds a lot better pattern matching. Though I’m not a fan of the language, I think it has the best version of a switch statement. I should call it pattern matching, which in language design, is the more general name for these feature. You’ll see in other languages like Haskell as well.

Perhaps that’s the biggest problem with switch. It feels like a stunted version of proper pattern matching. But that’s no reason to abandon it entirely and go back to if-else. Switch statements produce cleaner code as they express semantics, avoid duplication, and reduce the chance of errors.

Top comments (15)

Collapse
 
nektro profile image
Meghan (she/her)

I didnt get to point out but I'm glad you did in your article that the best way to format a switch statement is indented.

switch (x) {
  case 0: {
    func();
  }
  case 1: {
    func2();
  }
  ..
}
Collapse
 
thefern profile image
Fernando B 🚀 • Edited

Great read. I particularly don't use switch too often unless I use more than 3 or 4 ifs to make code cleaner. To make switch statements even cleaner combine them with enums if your language supports them.

Switch in kotlin are nice:

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}
Collapse
 
cubiclebuddha profile image
Cubicle Buddha

I know you and I already discussed this, but for the benefit of your readers: in TypeScript you can still get the benefit of exhaustiveness checking even with if statements. I describe how to do that here: dev.to/cubiclebuddha/is-defensive-...

Thank you for this article though because even though I’m not a fan of switch statements (for purely subjective reasons + the break issue that you mentioned), it’s really nice to hear from the opposite perspective. Also, you made a great point about hiding complexity with your expr example. Nice work. :)

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Defensive programming is a must. It's great if you can ensure coverage even with if-else in TypeScript.

Collapse
 
jdtjenkins profile image
jdtjenkins

Only evaluating the expression once, not having to use a temp variable. That's a big seller to me, that feels much neater. Great article!

Collapse
 
ben profile image
Ben Halpern

Fabulous post

Collapse
 
prahladyeri profile image
Prahlad Yeri

Another problem with switch is that you have to remember to provide the default: case otherwise you aren't covering all the cases! Its the same thing that happens when you forget the else block as shown in your if-else example. Its possible to leave out the default case in either scenario.

The break is another annoying thing and not just C/C++, even in Java and C#, you need to provide the break but I don't remember since its a long while since I've coded in those languages.

Python lacks a switch statement but I've never felt the need for it frankly. Ultimately, its just a more fancy variety of if statement!

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

C++ compilers will tell you when you've forgotten a default block. I didn't remember C# needing it, since it can't fall through to the next block.

Collapse
 
johannesvollmer profile image
Johannes Vollmer

Well, if you try to use a c-style switch statement that's definitely a problem. That's why I suggest you to look into the Rust way of switch: matching. It has all the good parts of switch but not the bad ones.

Collapse
 
johncip profile image
jmc • Edited

A switch indicates that you are mapping from an input value to a piece of code.

IMO when there's fallthrough, you're not mapping from values to code, but rather values to an offset in code. Like a jump table. I mention it not because I consider it to be all that dangerous but because in those languages it waters down the idea of switch-as-map.

But I agree on the whole. Often pattern matching is what you want (or the repeated application of a predicate, a la condp), and switch isn't always a perfect fit, but where it works it tends to be more explicit than the alternative.

Collapse
 
pinotattari profile image
Riccardo Bernardini • Edited

In Ada the corresponding of switch is case. As with many compiled languages, case is limited to discrete type (enumerations or integers). What I like of the Ada case is the absence of C fall-through (potential source of bugs) and the obligation of handling all the cases, possibly with a default branch, that is usually discouraged because it will not catch the error when you add a new enumeration value and forgot to handle it.

One could observe that it is easy to handle all the cases without default when you work with enumerations, but what about integers? Well, you can always define a subtype

  subtype Weekday is integer range 1..7;

  Tomorrow : Weekday;

  case Tomorrow is 
    when 1 => 
    ...
    when 7 =>
  end case;

Finally, if you try to use non-disjoint branches Ada Lovelace herself comes to you and slap your face with the INTERCAL manual. :-)

Oh, BTW, in the latest version of Ada you have also the "operator" version, very convenient and 40 dB more readable than usual "?:"

Name := (case Tomorrow is 
           when 1 => "Monday",
           when 2 => "Tuesday", 
           ...
           when 7 => "Sunday");
Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

In C/C++ they have fall-through, but more modern implementations don't have that. I believe you can even enable warnings in some C++ compilers to disallow fall-through.

Collapse
 
gklijs profile image
Gerard Klijs

Would it not be even cleaner to first get an enum from the expression (if it wasn't an enum already). And then call a function on the enum. It least I find it a lot easier to read, and anything specific to the enum is all together, instead of spread around.