Сортировка со своим правилом: usort
Представьте продавца, который расставляет товары на полке. Продавец не знает, как их упорядочить: по цене, по весу или по названию. Вы отдаёте ему карточку с правилом: «из двух товаров первым ставь более дешёвый».
Продавец берёт пару товаров, сверяется с карточкой и ставит их по порядку. Потом берёт следующую пару, и так до тех пор, пока полка не встанет как надо.
Так в PHP работает функция usort. Она сортирует массив, а правило, какой из двух элементов поставить раньше, получает в виде функции:
<?php
$nums = [30, 5, 12];
usort($nums, fn($a, $b) => $a <=> $b);
echo implode(' ', $nums), "\n"; // 5 12 30Давайте разберём программу по строкам:
$nums = [30, 5, 12];— список чисел вперемешку.usort($nums, …)получает массив и функцию сравнения — нашу карточку с правилом.fn($a, $b) => $a <=> $bполучает два элемента и отвечает, какой из них поставить раньше.- После
usortчисла в$numsстоят по возрастанию,implodeсклеивает их в строку5 12 30, иechoеё печатает.
Функция сравнения
Функция сравнения получает два элемента, $a и $b, и возвращает целое число:
- отрицательное —
$aставится раньше$b; - ноль — элементы равны;
- положительное —
$aставится позже$b.
Как вы уже знаете, оператор <=> возвращает -1, 0 или 1, а функции сравнения нужны именно такие ответы. Поэтому $a <=> $b — готовая функция сравнения по возрастанию.
Функция должна вернуть именно число. Если вернуть true или false, например $a > $b, PHP 8 выведет сообщение Deprecated: такая запись устарела.
usort вызывает функцию сравнения много раз, для разных пар элементов. Сколько будет вызовов и в каком порядке придут пары, решает PHP. Здесь картинка с продавцом неточна: продавец видит всю полку целиком, а функция сравнения видит только два элемента, которые ей передал usort.
Порядок сортировки
Одна и та же функция usort сортирует в любом порядке — всё решает функция сравнения.
По возрастанию
$a <=> $b: меньший элемент встаёт раньше, как в первом примере.
По убыванию
Чтобы первыми шли большие элементы, $a и $b меняют местами. С функцией fn($a, $b) => $b <=> $a тот же массив станет 30 12 5.
По полю записи
Элементы массива бывают записями — ассоциативными массивами. Тогда функция сравнивает одно поле записи:
$users = [
['name' => 'Olga', 'age' => 31],
['name' => 'Ivan', 'age' => 25],
['name' => 'Anna', 'age' => 31],
['name' => 'Petr', 'age' => 19],
];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
$names = [];
foreach ($users as $user) {
$names[] = $user['name'];
}
echo implode(' ', $names), "\n"; // Petr Ivan Olga AnnaРазберём по шагам:
$a['age'] <=> $b['age']сравнивает только возраст, а на имена не смотрит.- Записи встают от младшего к старшему: сначала Petr, потом Ivan, а затем Olga и Anna.
- У Olga и Anna возраст одинаковый,
31, и функция возвращает0. С PHP 8.0 сортировка устойчивая: равные элементы сохраняют исходный порядок, поэтому Olga остаётся перед Anna.
Сравнивать можно и вычисленное значение: strlen($a) <=> strlen($b) упорядочит строки по длине, от короткой к длинной.
usort меняет сам массив
usort не возвращает новый массив. Она получает массив по ссылке, как параметр с &, переставляет элементы прямо в нём и возвращает true.
Есть и вторая особенность: после usort ключи нумеруются заново, с 0. Ассоциативный массив теряет свои ключи и становится списком:
$prices = ['tea' => 40, 'milk' => 25, 'jam' => 90];
usort($prices, fn($a, $b) => $a <=> $b);
echo implode(' ', $prices), "\n"; // 25 40 90
echo implode(' ', array_keys($prices)), "\n"; // 0 1 2Цены встали по возрастанию, но ключей tea, milk и jam больше нет: на их месте 0, 1 и 2. Если ключи нужно сохранить, usort не подходит.
На собеседовании любят спрашивать, что возвращает usort. Ответ: true, а отсортированным становится сам переданный массив, и его ключи нумеруются заново.
Частая ошибка
Результат usort записали в переменную, как будто это отсортированный массив:
<?php
$nums = [30, 5, 12];
$sorted = usort($nums, fn($a, $b) => $a <=> $b); // ✗
echo implode(' ', $sorted), "\n";Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array, true givenВ $sorted лежит true, а implode ждёт массив и останавливает программу с TypeError. Отсортирован сам $nums, поэтому исправление — вызвать usort($nums, …) отдельной строкой и печатать $nums.
Главное
usort($items, $f)сортирует массив по функции сравнения$f, которая получает два элемента.- Функция сравнения возвращает отрицательное число, ноль или положительное;
$a <=> $bсортирует по возрастанию,$b <=> $a— по убыванию. usortменяет сам массив, возвращаетtrueи нумерует ключи заново.- С PHP 8.0 сортировка устойчива: равные элементы сохраняют исходный порядок.