Массивы как хэш-таблицы и подводные камни строк

«Что такое массив в PHP?» — вопрос-обманка: правильный ответ не «список» и не «словарь».

Массив в PHP — это упорядоченная хэш-таблица: набор пар «ключ → значение», который дополнительно помнит порядок вставки. Одна структура играет роль и списка, и словаря, и очереди, и множества.

Вопрос 1: «Как устроен массив внутри?»

Что проверяет интервьюер

Понимаете ли вы, что отдельного типа «список» в PHP нет. Отсюда следствия: доступ по ключу — в среднем O(1), in_array() — O(n), а foreach всегда идёт в порядке вставки, а не в порядке возрастания ключей. Начиная с PHP 7 движок хранит «упакованные» (packed) массивы с последовательными целыми ключами компактнее, но логическая модель остаётся хэш-таблицей.

Правила ключей

Ключом может быть только int или string. Всё остальное приводится:

<?php
$a = [];
$a["1"]  = 'строковый ключ "1"';
$a[1]    = 'целый ключ 1';
$a[true] = 'булев ключ true';
$a["01"] = 'строковый ключ "01"';
var_dump($a);

Вывод:

array(2) {
  [1]=>
  string(24) "булев ключ true"
  ["01"]=>
  string(32) "строковый ключ "01""
}

Три первые записи попали в один и тот же ключ 1: строка "1" — каноничная запись целого числа, поэтому она приводится к int; true тоже становится 1. А вот "01" каноничной записью не является — ключ остался строкой. Практический вывод: артикулы с ведущими нулями в качестве ключей безопасны, а «числовые» строки без нулей — нет.

Вопрос 2: «Чем array_merge() отличается от оператора +

<?php
$a = ['x' => 1, 'y' => 2];
$b = ['y' => 20, 'z' => 30];
print_r($a + $b);
print_r(array_merge($a, $b));
$nums = [3 => 'три', 1 => 'один', 2 => 'два'];
ksort($nums);
print_r($nums);

Вывод:

Array
(
    [x] => 1
    [y] => 2
    [z] => 30
)
Array
(
    [x] => 1
    [y] => 20
    [z] => 30
)
Array
(
    [1] => один
    [2] => два
    [3] => три
)

Оператор + при конфликте ключей оставляет значение левого массива и никогда не перенумеровывает. array_merge() при конфликте строковых ключей берёт значение правого массива, а числовые ключи перенумеровывает с нуля — из-за этого склейка списков товаров иногда неожиданно теряет исходные id.

Третий пример напоминает: сортировка меняет порядок обхода. sort() сбрасывает ключи, ksort()/asort() — сохраняют. Это тоже частый уточняющий вопрос.

Вопрос 3: «Почему strlen("Привет") возвращает 12?»

Потому что строка в PHP — это последовательность байтов, а не символов, и о кодировке она ничего не знает. В UTF-8 кириллическая буква занимает два байта.

<?php
$s = "Привет, мир";
echo strlen($s), "\n";
echo mb_strlen($s), "\n";
echo mb_strtoupper($s), "\n";
echo mb_substr($s, 0, 6), "\n";
echo str_contains($s, "мир") ? "да\n" : "нет\n";

Вывод:

20
11
ПРИВЕТ, МИР
Привет
да

Правило простое: всё, что связано с длиной, обрезкой и регистром, для многобайтовых строк делается через расширение mbstring (mb_strlen, mb_substr, mb_strtoupper). Функции поиска подстроки (str_contains, strpos) работают с байтами и для UTF-8 корректны, потому что кодировка самосинхронизирующаяся — байты одного символа не встречаются внутри другого. А вот substr() без mb_ легко разрежет строку посреди символа и выдаст «кракозябры» в превью текста.

Типичные ошибки кандидатов

  • Отвечают «массив — это список», не упоминая ключи и порядок вставки.
  • Считают in_array() быстрым поиском. Для больших наборов используйте isset($map[$key]): массив с ключами вместо массива со значениями превращает O(n) в O(1).
  • Не знают, что array_merge() перенумеровывает числовые ключи, и удивляются потере id.
  • Уверены, что foreach идёт по возрастанию ключей. Он идёт по порядку вставки.
  • Забывают про mb_-функции и режут UTF-8 посередине символа.
  • Путают array_key_exists() и isset(): второй вернёт false, если по существующему ключу лежит null.

Как ответить кратко

«Массив в PHP — упорядоченная хэш-таблица с ключами int или string; каноничные числовые строки-ключи приводятся к int, порядок обхода — порядок вставки. Оператор + сохраняет значения левого массива, array_merge перезаписывает строковые ключи правыми и перенумеровывает числовые. Строки — это байты, поэтому для UTF-8 длину и обрезку делаю через mb_-функции».

Проверьте себя
1. Что произойдёт при $a["1"] = 'a'; $a[1] = 'b'; $a[true] = 'c';?
AПолучатся три разных ключа
BБудет предупреждение о дублировании ключа
CВсе три записи попадут в один ключ int(1), останется 'c'
DКлючи "1" и 1 будут разными, а true создаст ключ "true"
2. Чем оператор + отличается от array_merge() при совпадении строковых ключей?
AОни полностью эквивалентны
BОператор + складывает значения по совпадающим ключам
Carray_merge() работает только с числовыми ключами
DОператор + оставляет значение левого массива, array_merge() берёт правое
3. Почему strlen("Привет") даёт 12, а mb_strlen("Привет") — 6?
Astrlen считает байты, а кириллица в UTF-8 занимает 2 байта на символ
Bstrlen добавляет символы переноса строки
Cmb_strlen обрезает строку до 6 символов
Dstrlen учитывает завершающий нулевой байт