Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Тип ключей в PHP

Наткнулись с коллегами на странную вещи в ПХП: тип имени свойства. Непонятно? Давайте посмотрим на код. Как вы думаете, есть ли какая-то разница в этих двух строках кода:

$first = (object) [ 1 => 1 ];
$second = new stdClass(); $second->{1} = 1;

В первом случае массив получает тип «объект» (и в этом случае ПХП создаёт экземпляр класса stdClass, используя ключи и значения массива как имя и значение свойства соответственно), во втором случае сразу создаётся экземпляр класса и его свойству с именем «1» присваивается значение.

Невооружённым взглядом различий не видно, но если попытаться обратиться к этим свойства, то сразу увидим что-то странное:

var_dump(
  $first->{1}, // выведет «Notice: Undefined property: stdClass::$1»
  $second->{1} // выведет значение «1»
);

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

$first = (object) [ 1 => 1 ];
$first->{1} =2;
var_dump($first);

/* у нас получился объект, у которого два свойства с одним именем,
но разными типами(!) имени, у одного он строка, у второго — число

object(stdClass)#1 (2) {
  [1]=>
  int(1)
  ["1"]=>
  int(2)
}
*/

var_dump((array) $first);

/* попытка преобразовать его в массив, приводит к появлению
неожиданного уродца — массива у которого два ключа с одним и тем
же именем, но разными типами…

array(2) {
  [1]=>
  int(1)
  ["1"]=>
  int(2)
}
*/

$arr = [ 1 => 1 ];
$arr["1"] = 2;
var_dump($arr);

/* …который вряд ли можно создать каким-то ещё способом

array(1) {
  [1]=>
  int(2)
}
*/

К свойству с числовым именем нельзя обратиться по имени, у ПХП просто нет конструкций для этого! Тем не менее, var_dump и foreach его видят! У массива всё ровно наоборот — по имени нельзя обратиться к свойству со строчным именем, если есть числовое.

Вот такие неожиданные открытия в ПХП. Я думаю, что это сделано для того, чтобы сохранить тип ключа массива в преобразованиях из объекта и обратно, но это решение теперь даёт странные сторонние эффекты. Например, перебирая такой массив через foreach можно столкнуться с ситуацией, когда удалить свойство из объекта по имени не удаётся. Или при итерации массива по известным ключам, мы будем получать одно и то же значение два раза (возвращаться всегда будет только то, которое записано по числовому имени).

В общем, масса интересных граблей насыпана в этом месте! Счастливой прогулки!

9 комментариев
vasa_c 2014

Прикольно. Не понятно, что значит «сохранить тип ключа массива в преобразованиях из объекта и обратно».

Евгений Степанищев (bolknote.ru) 2014

Комментарий для vasa_c:

В преобразованиях вида
$v = [ 1 => 1, «e» => «e» ];
$k = (object) $v;
$d = (array) $k;

они хотели, чтобы тип ключа в массиве сохранился. Т. е. единица тут должна быть числом, а «e» — строкой.

vstebunov@gmail.com 2014

вот еще крутые грабли:

$arr[null] = 3;
$arr[] = 7;
$arr[0] = 9;
print_r($arr);

Морозов (morozov.livejournal.com) 2014

Не сомневаюсь, что ты читал мануал, но про типы ключей массивов там написано: будучи использованными как ключи массива, строки, которые представляют собой валидное целочисленное значение, будут приведены к целочисленному значению. При этом не написано, во всех ли случаях. Может быть это просто баг? С трудом себе представляю, что в языке намеренно заложена такая функциональность.

Сергей Морозов (morozov.livejournal.com) 2014

vstebunov, здесь всё по мануалу: NULL приводится к пустой строке. В случае, если ключ не указан, ключом будет {максимальный целочисленный ключ} + 1 или 0, если такого ключа нет. Последнее присваивание явно указывает ключ 0 и переписывает предыдущее значение.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для morozov.livejournal.com:

Не сомневаюсь, что ты читал мануал, но про типы ключей массивов там написано: будучи использованными как ключи массива, строки, которые представляют собой валидное целочисленное значение, будут приведены к целочисленному значению. При этом не написано, во всех ли случаях. Может быть это просто баг? С трудом себе представляю, что в языке намеренно заложена такая функциональность.

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

vstebunov@gmail.com 2014

Сергей, ну раз по мануалу так по мануалу.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для vstebunov@gmail.com:

Мм… тут кажется понятно всё, нет?

Евгений Степанищев (bolknote.ru) 2014

Кстати, Reflection вообще не видит свойств, у которых имя числового типа.