Ассоциативный массив (так же говорят «словарь») - абстрактный тип данных, коллекция пар «ключ-значение», где каждый ключ уникален.
Если массив предназначен для хранения и обработки коллекций однотипных элементов, то ассоциативный массив подходит для совместного хранения и обработки разнотипных данных, которые, как правило, описывают что-то.
Например, с помощью ассоциативного массива удобно представить пользователя на Хекслете. Ключами в таком случае будут свойства: имя, пароль, город, дата рождения и, например, список курсов, которые пользователь прошел. Значениями, соответственно, будут данные конкретного пользователя. То же самое относится к любым другим объектам. С помощью ассоциативного массива можно описать любые сущности предметной области, например, заказ, курс, урок, топик в форуме, комментарий в проекте. В каждом случае будет своя структура, зависящая от тех свойств, которыми описывается конкретная сущность.
Кроме того, ассоциативные массивы используются как хранилища для конфигурационных параметров или как способ передать в функцию множество разнородных данных в виде одного параметра.
В курсе мы не только познакомимся с тем, как пользоваться ассоциативными массивами, но и рассмотрим их устройство. Познакомимся с понятием хеш-таблица, узнаем про коллизии и идеальную хеш-функцию.
В конце рассмотрим особенности связи между ассоциативными и обычными массивами в 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
Дополнительные материалы
PHP: Ассоциативные массивы → Ассоциативный массив в действии
Рассмотрим пример, в котором используется ассоциативный массив.
Напишем функцию, считающую количество вхождений каждого слова в предложение. Результатом работы этой функции является ассоциативный массив, в котором ключ — “слово”, а значение — “количество вхождений”. Пример:
<?php
$content = `cat dog cat eye see cat dog`;
$result = getWordsCount($content);
// => [
// 'cat' => 3,
// 'dog' => 2,
// 'eye' => 1,
// 'see' => 1
// ];
Логика работы функции выглядит так:
- Разбиваем строчку на слова
- Обходим массив слов и добавляем их в результат
- Если слово добавляется первый раз, то ставим в значение цифру 1
- Если слово уже было внутри результата, то текущее значение увеличиваем на 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']
Логика работы функции выглядит так:
- Обходим переданный массив
- Если значение в массиве совпадает с переданным, то добавляем ключ в результат
<?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
И хотя хеширование позволяет отображать ассоциативный массив на обычный массив, оно не лишено недостатков, с которыми нужно уметь работать.
Коллизии
Ключом в ассоциативном массиве может быть абсолютно любая строка (любой длины и состава). Другими словами, множество всех возможных ключей — бесконечно. В свою очередь, результат любой хешируемой функции — строка фиксированной длины, а значит множество всех выходных значений — конечно.
Из этого факта следует, что не для всех входных данных найдется уникальный хеш. На каком-то этапе возможно появление дублей (когда для разных значений получается один и тот же хеш). Такую ситуацию принято называть коллизией. Способов разрешения коллизий несколько, и каждому из них соответствует свой тип хеш-таблицы.
Коллизии не так редки, как может показаться. Убедиться в этом можно изучив парадокс дней рождений.
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. Сохраняет ключи у массива независимо ни от чего.
По сути в описании каждой функции, которая принимает на вход массив (или массивы) и возвращает массив, есть секция с пояснением, сохраняет ли ключи функция или нет, и как это поведение можно изменить.