Перейти к содержимому
Теория · Функции как значения · урок 7 из 8

Сортировка со своим правилом: usort

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

Продавец берёт пару товаров, сверяется с карточкой и ставит их по порядку. Потом берёт следующую пару, и так до тех пор, пока полка не встанет как надо.

Так в PHP работает функция usort. Она сортирует массив, а правило, какой из двух элементов поставить раньше, получает в виде функции:

PHP
<?php

$nums = [30, 5, 12];
usort($nums, fn($a, $b) => $a <=> $b);
echo implode(' ', $nums), "\n"; // 5 12 30

Давайте разберём программу по строкам:

  1. $nums = [30, 5, 12]; — список чисел вперемешку.
  2. usort($nums, …) получает массив и функцию сравнения — нашу карточку с правилом.
  3. fn($a, $b) => $a <=> $b получает два элемента и отвечает, какой из них поставить раньше.
  4. После 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.

По полю записи

Элементы массива бывают записями — ассоциативными массивами. Тогда функция сравнивает одно поле записи:

PHP
$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

Разберём по шагам:

  1. $a['age'] <=> $b['age'] сравнивает только возраст, а на имена не смотрит.
  2. Записи встают от младшего к старшему: сначала Petr, потом Ivan, а затем Olga и Anna.
  3. У Olga и Anna возраст одинаковый, 31, и функция возвращает 0. С PHP 8.0 сортировка устойчивая: равные элементы сохраняют исходный порядок, поэтому Olga остаётся перед Anna.

Сравнивать можно и вычисленное значение: strlen($a) <=> strlen($b) упорядочит строки по длине, от короткой к длинной.

usort меняет сам массив

usort не возвращает новый массив. Она получает массив по ссылке, как параметр с &, переставляет элементы прямо в нём и возвращает true.

Есть и вторая особенность: после usort ключи нумеруются заново, с 0. Ассоциативный массив теряет свои ключи и становится списком:

PHP
$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
<?php

$nums = [30, 5, 12];
$sorted = usort($nums, fn($a, $b) => $a <=> $b); // ✗
echo implode(' ', $sorted), "\n";
TEXT
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 сортировка устойчива: равные элементы сохраняют исходный порядок.