Reputation: 550
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?
Upvotes: 7
Views: 366
Reputation: 1630
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.
Upvotes: 3
Reputation: 550
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!
Upvotes: 0
Reputation: 57453
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, i.e., you really have count($opts)
+1 decision paths there, but the basic cyclomatic complexity only sees two.
A more awkward setup, that preserves complexity (i.e. 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 (i.e. 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.
Upvotes: 0
Reputation: 9142
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.
Upvotes: 2
Reputation: 1323
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';
}
}
Upvotes: 0