php - строк - конкатенация чисел




Конкатенация строк при увеличении (2)

EDIT: добавление примера Corbin: https://eval.in/34067

Очевидно, что в PHP есть ошибка. Если вы выполните этот код:

<?php

{
$a = 5;
echo ++$a.$a++;
}

echo "\n";

{
$a = 5;
$b = &$a;
echo ++$a.$b++;
}

echo "\n";

{
$a = 5;
echo ++$a.$a++;
}

Вы получаете:

66 76 76

Это означает, что один и тот же блок (1-й и 3-й одинаковый) кода не всегда возвращает тот же результат. По-видимому, ссылка и приращение помещают PHP в фиктивное состояние.

https://eval.in/34023

Это мой код:

$a = 5;
$b = &$a;
echo ++$a.$b++;

Должна ли она печатать 66?

Почему он печатает 76?


Хорошо. Это на самом деле довольно прямолинейное поведение, и оно связано с тем, как ссылки работают в PHP. Это не ошибка, а неожиданное поведение.

PHP внутренне использует copy-on-write. Это означает, что внутренние переменные копируются при их записи (так что $a = $b; не копирует память до тех пор, пока вы не измените один из них). Со ссылкой он никогда не копирует. Это важно для дальнейшего.

Давайте посмотрим на эти коды операций:

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 5
   3     1      ASSIGN_REF                                               !1, !0
   4     2      PRE_INC                                          $2      !0
         3      POST_INC                                         ~3      !1
         4      CONCAT                                           ~4      $2, ~3
         5      ECHO                                                     ~4
         6    > RETURN                                                   1

Первые два должны быть легко понятны.

  • ASSIGN - В принципе, мы оцениваем значение 5 в скомпилированной переменной с именем !0 .
  • ASSIGN_REF - Мы создаем ссылку от !0 до !1 (направление не имеет значения)

Пока это прямолинейно. Теперь идет интересный бит:

  • PRE_INC - это код операции, который фактически увеличивает эту переменную. Следует отметить, что он возвращает свой результат во временную переменную с именем $2 .

Итак, давайте посмотрим на исходный код PRE_INC при вызове с переменной:

static int ZEND_FASTCALL  ZEND_PRE_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval **var_ptr;

    SAVE_OPLINE();
    var_ptr = _get_zval_ptr_ptr_var(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);

    if (IS_VAR == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
        zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
    }
    if (IS_VAR == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
        if (RETURN_VALUE_USED(opline)) {
            PZVAL_LOCK(&EG(uninitialized_zval));
            AI_SET_PTR(&EX_T(opline->result.var), &EG(uninitialized_zval));
        }
        if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
        CHECK_EXCEPTION();
        ZEND_VM_NEXT_OPCODE();
    }

    SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

    if (UNEXPECTED(Z_TYPE_PP(var_ptr) == IS_OBJECT)
       && Z_OBJ_HANDLER_PP(var_ptr, get)
       && Z_OBJ_HANDLER_PP(var_ptr, set)) {
        /* proxy object */
        zval *val = Z_OBJ_HANDLER_PP(var_ptr, get)(*var_ptr TSRMLS_CC);
        Z_ADDREF_P(val);
        fast_increment_function(val);
        Z_OBJ_HANDLER_PP(var_ptr, set)(var_ptr, val TSRMLS_CC);
        zval_ptr_dtor(&val);
    } else {
        fast_increment_function(*var_ptr);
    }

    if (RETURN_VALUE_USED(opline)) {
        PZVAL_LOCK(*var_ptr);
        AI_SET_PTR(&EX_T(opline->result.var), *var_ptr);
    }

    if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

Теперь я не ожидаю, что вы поймете, что это делает сразу (это глубокий двигатель вуду), но давайте пройдем через него.

Первые два оператора if проверяют, является ли переменная «безопасной» для увеличения (первая проверяет, является ли это перегруженным объектом, вторая проверяет, является ли переменная особой переменной ошибки $php_error ).

Дальше это действительно интересный бит для нас. Поскольку мы изменяем значение, ему нужно предварительно форматировать copy-on-write. Поэтому он вызывает:

SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

Теперь, помните, мы уже установили переменную как ссылку выше. Таким образом, переменная не разделяется ... Это означает, что все, что мы делаем с ней здесь, также будет иметь значение $b ...

Затем переменная увеличивается ( fast_increment_function() ).

Наконец, он устанавливает результат как сам . Это копирование на запись еще раз. Он не возвращает значение операции, а действительную переменную . Итак, что возвращает PRE_INC по- прежнему является ссылкой на $a и $b .

  • POST_INC - Это ведет себя аналогично PRE_INC , за исключением одного ОЧЕНЬ важного факта.

Давайте проверим исходный код еще раз:

static int ZEND_FASTCALL  ZEND_POST_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    retval = &EX_T(opline->result.var).tmp_var;
    ZVAL_COPY_VALUE(retval, *var_ptr);
    zendi_zval_copy_ctor(*retval);

    SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
    fast_increment_function(*var_ptr);
}

На этот раз я отрезал все, что не интересно. Итак, давайте посмотрим, что он делает.

Во-первых, он получает временную переменную возврата ( ~3 в нашем коде выше).

Затем он копирует значение из своего аргумента ( !1 или $b ) в результат (и, следовательно, ссылка нарушена).

Затем он увеличивает аргумент.

Теперь помните, что аргумент !1 - это переменная $b , которая имеет ссылку на !0 ( $a ) и $2 , что, если вы помните, было результатом PRE_INC .

Итак, у вас это есть. Он возвращает 76, потому что ссылка поддерживается в результате PRE_INC.

Мы можем доказать это, выставив копию, предварительно назначив pre-inc временной переменной (через обычное присвоение, которое нарушит ссылку):

$a = 5;
$b = &$a;
$c = ++$a;
$d = $b++;
echo $c.$d;

Что работает, как вы ожидали. Proof

И мы можем воспроизвести другое поведение (вашу ошибку), введя функцию для поддержания ссылки:

function &pre_inc(&$a) {
    return ++$a;
}

$a = 5;
$b = &$a;
$c = &pre_inc($a);
$d = $b++;
echo $c.$d;

Который работает так, как вы его видите (76): Proof

Примечание: единственная причина для отдельной функции здесь заключается в том, что парсер PHP не любит $c = &++$a; , Поэтому нам нужно добавить уровень косвенности через вызов функции, чтобы сделать это ...

Причина, по которой я не считаю это ошибкой, заключается в том, что так работают ссылки. Предварительное приращение переменной, на которую ссылается, вернет эту переменную. Даже переменная без ссылки должна возвращать эту переменную. Возможно, это не то, что вы ожидаете здесь, но оно работает очень хорошо практически в каждом другом случае ...

Базовая точка

Если вы используете ссылки, вы делаете это неправильно примерно в 99% случаев. Поэтому не используйте ссылки, если они вам не нужны. PHP намного умнее, чем вы думаете при оптимизации памяти. И ваше использование ссылок действительно мешает тому, как это может работать. Поэтому, пока вы думаете, что можете писать смарт-код, вы действительно собираетесь писать менее эффективный и менее дружелюбный код в подавляющем большинстве случаев ...

И если вы хотите узнать больше о ссылках и о том, как работают переменные в PHP, просмотрите одно из моих видео на YouTube по этому вопросу ...





pre-increment