[Hexlet] PHP: Ассоциативные массивы

Ассоциативный массив (так же говорят «словарь») - абстрактный тип данных, коллекция пар «ключ-значение», где каждый ключ уникален.

Если массив предназначен для хранения и обработки коллекций однотипных элементов, то ассоциативный массив подходит для совместного хранения и обработки разнотипных данных, которые, как правило, описывают что-то.

Например, с помощью ассоциативного массива удобно представить пользователя на Хекслете. Ключами в таком случае будут свойства: имя, пароль, город, дата рождения и, например, список курсов, которые пользователь прошел. Значениями, соответственно, будут данные конкретного пользователя. То же самое относится к любым другим объектам. С помощью ассоциативного массива можно описать любые сущности предметной области, например, заказ, курс, урок, топик в форуме, комментарий в проекте. В каждом случае будет своя структура, зависящая от тех свойств, которыми описывается конкретная сущность.

Кроме того, ассоциативные массивы используются как хранилища для конфигурационных параметров или как способ передать в функцию множество разнородных данных в виде одного параметра.

В курсе мы не только познакомимся с тем, как пользоваться ассоциативными массивами, но и рассмотрим их устройство. Познакомимся с понятием хеш-таблица, узнаем про коллизии и идеальную хеш-функцию.

В конце рассмотрим особенности связи между ассоциативными и обычными массивами в php.

PHP: Ассоциативные массивы Синтаксис

Синтаксически работа с ассоциативными массивами очень похожа на то, как мы работали с обычными массивами. Различия кроются в деталях.

Создание:

<?php

$user = ['name' => 'Vasya', 'married' => true, 'age' => 25];

Общий принцип такой: внутри квадратных скобок через запятую перечисляются пары ключ-значения в формате key => значение . Тип значения может быть любым, ключ — обычно, строка.

Если ключей много, то определение можно растянуть на несколько строк:

<?php

$user = [
    'name' => 'Vasya',
    'married' => true,
    'age' => 25
];

Так же, как и с обычным массивом, ассоциативный массив можно создать пустым:

<?php

$user = [];

Синтаксически эта запись совпадает со способом создания обычного (пустого) массива. Возникает вопрос: как интерпретатор различает типы массивов? Хитрость в том, что в PHP индексированных массивов нет, все массивы — ассоциативные. Но если работать с ними так, как мы делали в предыдущем курсе, то он ведет себя как индексированный массив (почти, различия всё же есть). Подробнее этот вопрос разбирается позже, в одном из уроков.

Извлекаются элементы из ассоциативного массива так:

<?php

$user['name']; // Vasya
$user['age']; // 25

Обращение к несуществующему ключу порождает PHP Notice , что сродни ошибке. В реальных программах, если нет уверенности, что ключ существует, нужно обязательно производить его проверку используя уже знакомую нам функцию array_key_exists .

<?php

$user = ['name' => 'Vasya', 'married' => true, 'age' => 25];

if (array_key_exists('name', $user)) {
    print_r('yeah!');
} else {
    print_r('no');
}
// => yeah!

Для создания и обновления значений ключей в ассоциативном массиве используется одна и та же операция:

<?php

$user = ['name' => 'Vasya', 'married' => true, 'age' => 25];

$user['married'] = false;
$user['surname'] = 'Petrov';

print_r($user);
// => ['name' => 'Vasya', 'married' => false, 'age' => 25, 'surname' => 'Petrov']

Так как в PHP ассоциативный массив и обычный — одно и то же, все функции, которые мы использовали в предыдущем курсе, применимы и тут. Например, для удаления unset , а для получения размера sizeof .

Значением ключа ассоциативного массива может быть все, что угодно включая, опять же, массив.

<?php

$user = ['name' => 'Vasya', 'married' => true, 'age' => 25];

$user['friends'] = ['Kolya', 'Petya'];

$user['children'] = [
    ['name' => 'Mila', 'age' => 1],
    ['name' => 'Petr', 'age' => 10]
];

В этом случае обращение к вложенным элементам происходит так:

<?php

$user['friends'][1]; // Petya
$user['children'][0]['name']; // Mila

То есть после извлечения элемента к нему можно сразу же обращаться как к массиву без необходимости создавать промежуточные переменные. Иногда переменные все же необходимы, и в этом случае код будет таким:

<?php

$friends = $user['friends'];
$friends[0]; // Kolya

К элементам ассоциативных массивов можно обращаться точно так же как и к элементам обычных массивов используя переменные:

<?php

$key = 'friends';
$friends = $user[$key];
$friends[0]; // Kolya

Дополнительные материалы

  1. Официальная документация

PHP: Ассоциативные массивы Ассоциативный массив в действии

Рассмотрим пример, в котором используется ассоциативный массив.

Напишем функцию, считающую количество вхождений каждого слова в предложение. Результатом работы этой функции является ассоциативный массив, в котором ключ — “слово”, а значение — “количество вхождений”. Пример:

<?php

$content = `cat dog cat eye see cat dog`;

$result = getWordsCount($content);
// => [
//     'cat' => 3,
//     'dog' => 2,
//     'eye' => 1,
//     'see' => 1
// ];

Логика работы функции выглядит так:

  1. Разбиваем строчку на слова
  2. Обходим массив слов и добавляем их в результат
  3. Если слово добавляется первый раз, то ставим в значение цифру 1
  4. Если слово уже было внутри результата, то текущее значение увеличиваем на 1
<?php

function getWordsCount($content)
{
    // Разбиваем на слова
    $words = explode(' ', $content);
    $result = [];
    foreach ($words as $word) {
        if (!array_key_exists($word, $result)) {
            // Инициализация при первом упоминании
            $result[$word] = 1;
        } else {
            $result[$word]++;
        }
    }

    return $result;
}

PHP: Ассоциативные массивы Foreach

К ассоциативным массивам в PHP применим только один вид циклов — foreach . Причем, он работает одинаково для индексированных и ассоциативных массивов.

<?php

$course = ['name' => 'JS: React', 'slug' => 'js-react'];

foreach ($course as $key => $value) {
    print_r("{$key}: {$value}");
}

// => name: JS: React
// => slug: js-react

Если ключ не нужен, то часть $key => можно опустить и тогда цикл станет таким:

<?php

foreach ($course as $value) {
    print_r($value);
}

// => JS: React
// => js-react

Способность обходить ассоциативный массив циклом — одна из особенностей PHP, отличающая его от большинства других популярных языков программирования. Это связано с тем, что массивы в PHP — нечто среднее между обычными массивами и ассоциативными массивами, реализованное в рамках одного типа Array.

Рассмотрим пример. Реализуем функцию findKeys , которая возвращает список ключей массива, значение которых равно переданному значению:

<?php

$lessonMembers = [
  'syntax' => 3,
  'using' => 2,
  'foreach' => 10,
  'operations' => 10,
  'destructuring' => 2,
  'array' => 2,
];

$result = findKeys($lessonMembers, 10);
// => ['foreach', 'operations']

$result = findKeys($lessonMembers, 3);
// => ['syntax']

Логика работы функции выглядит так:

  1. Обходим переданный массив
  2. Если значение в массиве совпадает с переданным, то добавляем ключ в результат
<?php

function findKeys(array $data, $expectedValue)
{
    $result = [];
    foreach ($data as $key => $value) {
        if ($value === $expectedValue) {
            $result[] = $key;
        }
    }

    return $result;
}

Обход ассоциативного массива с помощью foreach всегда происходит в том же порядке, в котором элементы добавлялись в массив.

PHP: Ассоциативные массивы Популярные функции для работы с ассоциативными массивами

Общий список функций для работы с ассоциативными массивами довольно большой. Можно выделить несколько функций, которые встречаются во всех языках программирования, имеющих ассоциативные массивы.

array_keys

Функция array_keys извлекает из ассоциативного массива ключи и создает из них массив.

<?php

$data = ['first_name' => 'Mark', 'last_name' => 'Smith'];

$keys = array_keys($data);
// => ['first_name', 'last_name']

Типичное применение данной функции в языках отличных от PHP — обход ассоциативного массива:

<?php

$data = ['first_name' => 'Mark', 'last_name' => 'Smith'];

$keys = array_keys($data);
foreach($keys as $key) {
    print_r($data[$key]);
}

В PHP то же самое самое делается прямым обходом ассоциативного массива, но знать про функцию все равно полезно. Например, в JSON (как и в языках, отличных от PHP) массив и ассоциативный массив — разные типы данных.

{
  "autoload": {
    "files": [
      "src/Arrays.php"
    ]
  },
  "config": {
    "vendor-dir": "/composer/vendor"
  }
}

Выше files — обычный массив, а config — ассоциативный.

array_values

Функция array_values извлекает из ассоциативного массива значения и создает из них массив.

<?php

$data = ['first_name' => 'Mark', 'last_name' => 'Smith'];

$keys = array_values($data);
// => ['Mark', 'Smith']

array_merge

Наиболее интересная функция — array_merge или так называемое слияние. Слияние двух массивов порождает новый массив, в котором поверх первого массива накладывается второй по следующим правилам:

  • Если в первом массиве есть ключ, которого нет во втором, то он остается
  • Если в первом и во втором массиве есть один и тот же ключ, то его значением становится значение из второго массива
  • Если в первом массива нет ключа, который есть во втором, то он добавляется

Операция слияния не коммутативна, так же, как и вычитание. Изменение порядка аргументов (перемена массивов) приведет к другому результату.

<?php

$data1 = [
    'first_name' => 'Mark',
    'last_name' => 'Polo',
];

$data2 = [
    'last_name' => 'Brin',
    'age' => 15,
];

$result = array_merge($data1, $data2);
// => [
//     'first_name' => 'Mark',
//     'last_name' => 'Brin',
//     'age' => 15,
// ]

PHP: Ассоциативные массивы Destructuring

Напомню, что деструктуризация (дестракчеринг) — специальный синтаксис, позволяющий извлекать части из составных данных. Самый простой пример, который мы рассмотрели, заключается в извлечении значений массива состоящего из двух элементов.

<?php

[$firstName, $lastName] = $arr;

На части можно раскладывать не только индексированные, но и ассоциативные массивы, извлекая из них значения по определенным ключам.

<?php

$person = ['first' => 'Rasmus', 'last' => 'Lerdorf', 'manager' => true];

// Порядок извлечения не важен
['last' => $lastname, 'first' => $firstname] = $person;

Теперь переменные $lastname и $firstname содержат соответствующие значения. Имена самих переменных выбираются произвольно, главное — совпадение по ключам.

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

<?php

$options = ['enabled' => true, 'compression' => ['algo' => 'gzip']];

[
    'enabled' => $enabled,
    'compression' => [
        'algo' => $compressionAlgo
    ]
] = $options;

Дестракчеринг ассоциативного массива можно комбинировать с дестракчерингом индексированного.

<?php
$x = ['o' => [1, 2, 3]];
['o' => [$a, $b, $c]] = $x;

$y = ['o' => [[1, 2, 3], ['what' => 'WHAT']]];
['o' => [[$one, $two, $three], ['what' => $what]]] = $y;

Дестракчеринг допустим и в циклах:

<?php

$persons = [
    ['first' => 'Rasmus', 'last' => 'Lerdorf'],
    ['first' => 'Fabien', 'last' => 'Potencier'],
    ['first' => 'Taylor', 'last' => 'Otwell']
];

foreach ($persons as ['first' => $firstname, 'last' => $lastname]) {
    var_dump($firstname, $lastname);
}

Extract

Кроме описанного выше, в PHP существует еще один способ дестракчеринга, который на первый взгляд кажется проще. Вызов функции extract с переданным ассоциативным массивом приводит к тому, что создаются переменные с именами ключей, в которые записываются значения из массива.

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

Переменные всегда создаются для всех ключей массива. Они засоряют локальное окружение и могут приводить к ошибкам если пересекутся с названиями уже существующих переменных.
Безопасность. Если содержимое массива приходит извне (из формы или базы данных), то есть потенциальная вероятность попадания в массив ключа, который приведет к перезаписыванию значения существующей переменной.
Неявное создание переменных вообще само по себе странная вещь (и нигде не встречается!). Такой код значительно сложнее в анализе.

PHP: Ассоциативные массивы Хеш-таблицы

Ассоциативный массив — абстрактный тип данных. У него есть и другие названия: «словарь», «мап». В разных языках ему соответствуют разные типы данных, названия которых имеют мало общего с названием ADT. Например:

  • Ruby - Hash
  • Lua - Table
  • Python - Dictionary
  • JS - Object
  • Elixir/Java - Map

Ассоциативный массив, в отличие от индексированного массива, нельзя положить в память «как есть». Непонятно, как хранить ключи и связывать их со значениями. Для реализации ассоциативных массивов используют, так называемые, хеш-таблицы.

Хэш-таблица — это структура данных, реализующая интерфейс ассоциативного массива. Существуют два основных варианта хеш-таблиц: с цепочками и открытой адресацией. Независимо от выбранного варианта, основа любой хеш-таблицы — индексированный массив, в котором и хранится вся информация и хеширование, о котором мы поговорим ниже. Остальная логика сводится к разрешению коллизий.

Хеширование

Любая операция внутри хеш-таблицы начинается с того, что ключ каким-то образом преобразуется в индекс массива. Именно так производятся все операции. Сначала вычисляется индекс на основе ключа, дальше туда либо записываются данные, либо читаются.

Преобразование ключа в индекс массива выполняется с помощью хеширования. Хеширование — операция, которая преобразует любые входные данные в строку фиксированной длины. Функция, реализующая алгоритм преобразования, называется «хеш-функцией», а результат называют «хешем» или «хеш-суммой».

С хешированием мы встречаемся в разработке крайне часто. Например, идентификатор коммита в git 0481e0692e2501192d67d7da506c6e70ba41e913 ни что иное, как хеш, полученный в результате хеширования.

Самый простой способ хешировать данные на PHP — использовать функцию crc32 :

<?php

$checksum = crc32('The quick brown fox jumped over the lazy dog.');
// => 2191738434

И хотя хеширование позволяет отображать ассоциативный массив на обычный массив, оно не лишено недостатков, с которыми нужно уметь работать.

Коллизии

Ключом в ассоциативном массиве может быть абсолютно любая строка (любой длины и состава). Другими словами, множество всех возможных ключей — бесконечно. В свою очередь, результат любой хешируемой функции — строка фиксированной длины, а значит множество всех выходных значений — конечно.

Из этого факта следует, что не для всех входных данных найдется уникальный хеш. На каком-то этапе возможно появление дублей (когда для разных значений получается один и тот же хеш). Такую ситуацию принято называть коллизией. Способов разрешения коллизий несколько, и каждому из них соответствует свой тип хеш-таблицы.

hash table collision

Коллизии не так редки, как может показаться. Убедиться в этом можно изучив парадокс дней рождений.

PHP: Ассоциативные массивы Массив и Ассоциативный Массив

В PHP есть только один тип данных для массивов — Array. Его уникальность заключается в том, что с одной стороны он работает как обычный массив, а с другой — как ассоциативный. Зависит от того, как его используют.

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

Самый простой пример — JSON. В JSON массив и ассоциативный массив — разные сущности. Если конвертировать JSON в массив, то эта информация теряется. Если мы не знаем структуру JSON, то у нас нет простого способа понять, что перед нами — массив или ассоциативный массив. В интернете с подобным сталкиваются постоянно и предлагают такой способ, как анализ ключей. Если они все числовые, то считаем, что массив, иначе — ассоциативный массив. Конвертация из массива в JSON сопряжена с такими же проблемами. Как понять, во что конвертировать переданный массив?

Другая проблема заключается в том, что достаточно легко ошибиться с типом массива и начать его использовать не по назначению:

<?php

$data = [];
$data[] = 10;
$data['key'] = 'value';
$data[] = 'hi!';

Первое удивление — код работает! Теперь попробуйте догадаться, что находится внутри $data .

<?php

print_r($data);
// => Array
// (
//     [0] => 10
//     [key] => value
//     [1] => hi!
// )

Из этого вывода должно быть понятно, что индексированных массивов в PHP нет. Есть упорядоченные ассоциативные массивы, с операцией [] = : добавить элемент с автоматическим присвоением ключа.

<?php

$data = ['key' => 'value'];
$data[] = 'console';

// => Array
// (
//     [key] => value
//     [0] => console
// )

Но самое неудобное — функции которые могут сохранять, а могут не сохранять ключи. Обычно в таких функциях есть дополнительный параметр флаг preserve_keys , который меняет описанное поведение.

  • array_reverse array array_reverse ( array $array [, bool $preserve_keys = FALSE ] ) preserve_keys If set to TRUE numeric keys are preserved. Non-numeric keys are not affected by this setting and will always be preserved.
  • array_uniqueNote that keys are preserved. If multiple elements compare equal under the given sort_flags, then the key and value of the first equal element will be retained.
  • unset. Сохраняет ключи у массива независимо ни от чего.

По сути в описании каждой функции, которая принимает на вход массив (или массивы) и возвращает массив, есть секция с пояснением, сохраняет ли ключи функция или нет, и как это поведение можно изменить.

1 симпатия