简体   繁体   中英

Unexpected PHP Switch Behavior

I was running some unit tests and ran across an unexpected behavior with a switch statement I was using. I've isolated the condition below.

function test($val)
{
    switch($val)
    {
       case 'a':
       case 'b':
          return 'first';
       break;
       case 'c':
          return 'second';
       break;
       default:
          return 'third';
    }
}

here are my first round of tests:

test('a') => 'first'
test('b') => 'first'
test('c') => 'second'
test('d') => 'third'    
test('0') => 'third'
test('1') => 'third'
test('true')  => 'third'
test('false') => 'third'

This is pretty self evident right? ok now check these out:

test(0)     => 'first'  // expected 'third'
test(1)     => 'third'
test(true)  => 'first'  // expected 'third'
test(false) => 'third'
test(null)  => 'third'
test([])    => 'third'

What's with the weird results with 0 and true? I would chalk it up to loose typing if 1/true and 0/false returned the same values. But they don't!

If I convert the value to a (string) then the switch works as intended.

test((string) 0)     => 'third'
test((string) 1)     => 'third'
test((string) true)  => 'third'
test((string) false) => 'third'

I don't understand why the switch wont "work" as I intended without using "(string)"

Can someone explain why this is happening?

This is expected behavior. When doing comparisons, PHP will alter a value's type in its search for a match.

test(0)     => 'first'  // 'a' is altered into int 0 and therefore matches
var_dump((int) 'a'); // results 'int(0)'

test(true)  => 'first'  // both true and 'a' are truthy statements therefore matches.
if ('a' == true) echo "its true";

PHP is a weakly typed language and that bites you in the butt sometimes. You may consider re-factoring the switch into an if/else if/else structure and use the === operator for strong comparisons.

Per PHP's documentation:

Note that switch/case does loose comparison.

http://php.net/manual/en/control-structures.switch.php

If you want to do type comparison, you will need to restructure your code. Example:

function test($val)
{
    if($val === 'a' || $val === 'b') 
        return 'first';

    if($val === 'c') 
        return 'second';

    return 'third';
}

Notice how I don't have any else 's. This is because every statement returns something... Otherwise the function will return third by default.

You've got simple answers: loose comaprison. But what is solution? If you want compare numbers don't use strings. If you want boolean, use boolean. Try to validate (or cast) variables before calling function/method. Write your code like in strongly typed language, if you want int you can do something like this:

/**
 * @param int $val 
 */
function test($val)
{
    //exception 
    if (!is_int($val)) {
       throw new InvalidArgumentException('$val expected to be int');
    }
    //or cast - but may beahave unexpected
    $val = (int)$val

    switch($val)
    {
       case 0:
          return 'first';
       break;
       case 1:
          return 'second';
       break;
       default:
          return 'third';
    }
}

PHP is weakly (or loosely) typed , so you need to cast $val appropriately and/or assert its type if necessary:

/**
 * @param string $val
 */

function test($val)
{
    assert('is_string($val)', 'expecting string');
    switch((string)$val) {
        case 'a':

}

An alternative is to use a dictionary:

$opts = [
    'a' => 'first',
    'b' => 'first',
    ...
];
foreach ($opts as $opt => $response) {
    if ($opt === $val) {
        return $response;
    }
}
return 'default';

Since you're talking of running tests, be advised that the above has the probably unwanted side effect of masking the cyclomatic complexity, ie, you really have count($opts) +1 decision paths there, but the basic cyclomatic complexity only sees two.

A more awkward setup, that preserves complexity (ie you get correct code coverage from tests) could be

private function testA($val) {
    return 'first';
}
private function testB($val) {
    return $this->testA($val); // or 'first' again
}
...

public function test($val) {
    if (!is_class_method($this, $method = 'test' . $val)) {
        $method = 'testDefault';
    }
    $this->$method($val);
}

The above has the drawback of not letting you have all the options in a single unity (ie the switch), and can only be adopted if you have values that can be used in a function name, and the check is case insensitive. But after trying several ways, I'm finding this a workable compromise.

Short answer: cast the input value

Thank you for all the replies. The following link was very helpful in wrapping my head around what was going on.

http://php.net/manual/en/types.comparisons.php#types.comparisions-loose

The method I was testing was never intended to be used with integers. I just happened to throw a few different types just for fun and came across the unintended behavior by accident.

After reading all these responses I will take a different approach and not use a switch statement. I think a dictionary approach that @lserni mentioned is the way to go for this specific implementation. However if I wanted to keep the same code, a quick fix would be to do what @Styx suggested and cast the value as a (string).

Thanks!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM