Pull to refresh

Порядок выполнения тернарного оператора

Reading time3 min
Views36K
В php есть интересная особенность тернарного оператора — специфический и уникальный порядок выполнения.

$ python -c "print 1 if true else 2 if true else 3 if true else 4 if true else 5"
 1

$ node   -e "      true ? 1 : true ? 2 : true ? 3 : true ? 4 : 5"
 1

$ perl   -e "print true ? 1 : true ? 2 : true ? 3 : true ? 4 : 5"
 1

$ ruby   -e "print true ? 1 : true ? 2 : true ? 3 : true ? 4 : 5"
 1

$ php    -r "print true ? 1 : true ? 2 : true ? 3 : true ? 4 : 5;"
 4
Java и C++ тоже вернут 1


А какая вообще разница?


Я знаю об этом интересном нюансе довольно давно, но буквально вчера обнаружил ошибку в одном из открытых исходников: автор явно не знал об этом нюансе и попался. Потому, данная статья служит всего-лишь предупреждением. Ведь, если программист ожидает от php такого же поведения, как от других языков — может попасть в халепу.

Такой приём очень удобный для задания значений, зависимо от условий. Изящная замена if-else. Например:
value = isCondFirst()  ? valueFirst()  :
        isCondSecond() ? valueSecond() :
        isCondThird()  ? valueThird()  :
                         valueDefault();
 
/********** Вместо **********/

if (isCondFirst()) {
	value = valueFirst();
} else if (isCondSecond()) {
	value = valueSecond();
} else if (isCondThird()) {
	value = valueThird();
} else {
	value = valueDefault();
}


Как избежать ошибки?


Первый способ — не использовать тернарный оператор в php.
Второй — прямо указывать порядок выполнения с помощью скобок:

$ php -r "print true ? 1 : (true ? 2 : (true ? 3 : (true ? 4 : 5)));"
 1

Чем-то напоминает Лисп, не так ли?

Почему оно вообще так происходит?


Давайте разберемся в порядке выполнения тернарного оператора на примере JavaScript vs PHP
Напишем два тестовых скрипта, чтобы понять, как работает каждый из языков.

На всякий случай поясню, что при $foo = $lambda('fooMsg', 'fooReturn'), $foo содержит в себе функцию, которая при вызове выведет в консоль сообщение 'fooMsg' и вернет значение 'fooReturn'

$ cat ternary.js
var lambda = function (logMsg, returnValue) {
        return function () {
                console.log(logMsg);
                return returnValue;
        };
};

var cond = {
        first : lambda('cond.first' , true),
        second: lambda('cond.second', true),
        third : lambda('cond.third' , true)
};


var value = {
        first  : lambda('value.first'  , 'first'),
        second : lambda('value.second' , 'second'),
        third  : lambda('value.third'  , 'third'),
        default: lambda('value.default', 'default')
};

console.log( 'result: ',
        cond.first()  ? value.first()  :
        cond.second() ? value.second() :
        cond.third()  ? value.third()  : value.default()
);


$ node ternary.js
cond.first
value.first
result: first


$ cat ternary.php
<?php

$lambda = function ($logMsg, $returnValue) {
        return function () use ($logMsg, $returnValue) {
                echo $logMsg . PHP_EOL;
                return $returnValue;
        };
};

$cond = array(
        'first' => $lambda('cond.first' , true),
        'second'=> $lambda('cond.second', true),
        'third' => $lambda('cond.third' , true),
);

$value = array(
        'first'  => $lambda('value.first'  , 'first'),
        'second' => $lambda('value.second' , 'second'),
        'third'  => $lambda('value.third'  , 'third'),
        'default'=> $lambda('value.default', 'default'),
);

echo 'result: ' . (
	$cond['first']()  ? $value['first']()  :
	$cond['second']() ? $value['second']() :
	$cond['third']()  ? $value['third']()  : $value['default']()
) . PHP_EOL;

?>

$ php ternary.php
cond.first
value.first
value.second
value.third
result: third


Какой из этого результата можно сделать вывод. Javascript разбирает тернарный оператор вполне логично. Сначала проверяет самое левое условие. Если оно верно, то выполняет и возвращает левую часть после первого двоеточия, если не верно, то правую.
(cond.first() ? value.first() :
	(cond.second() ? value.second() :
		(cond.third() ? value.third() :
			(value.default()))));

/********
 * ===> 
 */

true ? 'value.first' : /* ignored */;


PHP мыслит оригинально.

(
	(
		cond.first() ?
			value.first() :
			cond.second()
	) ?
		value.second() :
		cond.third()
) ?
	value.third() :
	value.default();

/********
 * ===> 
 */

(
	(
		'value.first'
	) ?
		value.second() :
		cond.third()
) ?
	value.third() :
	value.default();
	
/********
 * ===> 
 */

(
	'value.second'
) ?
	value.third() :
	value.default();

/********
 * ===> 
 */

'value.third'


Что подтверждается в официальном мануале:
$a = true ? 0 : true ? 1 : 2; // (true ? 0 : true) ? 1 : 2 = 2


Заключение


В общем PHP себя снова показал «с лучшей стороны», но проблема далеко не критическая. Главное — знать про неё и быть осторожным с тернарным оператором в PHP.
Tags:
Hubs:
Total votes 90: ↑70 and ↓20+50
Comments110

Articles