Хватит писать foreach()

По крайней мере в php :) Обращаюсь к php-разработчикам – подумайте, сколько раз за рабочий день вы пишите for() и foreach()? Я, например, довольно много :) Сейчас я пишу на php 5.3, а в нем есть очень удобные средства для того, чтобы обойтись без обхода массива в цикле – это функции для работы с массивами, которые принимают callback в качестве аргумента и замыкания

Я говорю о array_map(), array_reduce(), array_filter() и uasort() (ну и похожих на них). Использование этих функций – неплохой способ обойтись без обхода массивов в цикле.
В PHP4 и PHP5.2 использование коллбэков выглядело весьма и весьма громоздко. Вы должны были определить функцию (или метод), а затем передать ее имя (или массив, содержащий имя класса и имя публичного или статического метода) в качестве аргумента. Можно было извернуться через create_function, но все равно это выглядело громоздко.
В PHP5.3 вы можете определить анонимную функцию где-нибудь в коде (или даже непосредственно в вызове функции). Эти функции выглядят как обычные переменные и вы спокойно можете передавать их как аргумент.
Я, конечно, не поклонник смешивания функционального и объектно-ориентированного подхода (тем более, что в PHP оба имеют проблемы с реализацией :)), но подобный прием поможет избавиться в коде от шума, который создают циклы, предназначенный для обхода массивов.

// обычный массив простых чисел до 20
$primeNumbers = array(2, 3, 5, 7, 11, 13, 17, 19);

// array_map() применяет callback-функцию к каждому элементу массива и
// возвращает результат
$square = function($number) {
    return $number * $number;
};
$squared = array_map($square, $primeNumbers);
echo "Квадраты простых чисел: ",
     implode(', ', $squared), "\n";

// array_reduce() рекурсивно применяет фнукцию к парам значений
// и превращает массив в одно единственное значение
// Очень похоже на array_sum(),
// но использование callback открывает неплохие перспективы
$sum = function($a, $b) {
    return $a + $b;
};
$total = array_reduce($primeNumbers, $sum);
echo "Сумма простых чисел: ", $total, ".\n";

// array_filter() оставляет в массиве значения,
// которые удовлетворяют условию
$even = function($number) {
    return $number % 2 == 0;
};
$evenPrimes = array_filter($primeNumbers, $even);
echo "Четные простые числа: ",
     implode(', ', $evenPrimes), ".\n";

// uasort() сортирует массив и
// сохраняет связь междя ключом и значением
// похожа на asort(), но использует callback
$compare = function($a, $b) {
    if ($a == $b) {
        return 0;
    }
    return ($a > $b) ? -1 : 1;
};
uasort($primeNumbers, $compare);
echo "Упорядоченный по убыванию массив простых чисел: ",
     implode(', ', $primeNumbers), ".\n";

UPDATE:
Обещанные замеры производительности. Тест, конечно, весьма синтетический, но и он весьма нагляден. Я, честно говоря, был слегка удивлен.
Код теста:

echo "We're using the PHP " . phpversion() . "\n";

$array = $array2 = range(0, 10000, 1);

echo "Array contains " . count($array) . " elements\n";

$t1 = microtime(1);
echo "BEGIN array_map\n";

array_map(
        function($number){
                return $number * $number;
        },
        $array
);
$t2 = microtime(1);
$res = $t2 - $t1;
echo "END array_map: " . round($res, 4) . "\n\n";

$t1 = microtime(1);
echo "BEGIN foreach:\n";
foreach ($array2 as $key => $value){
        $array2[$key] = $value * $value;
}
$t2 = microtime(1);
$res = $t2 - $t1;
echo "END foreach: " . round($res, 4) . "\n\n";

И вот результаты:

[boombick@srv01 /tmp]$ php -f test.php
We're using the PHP 5.3.1
Array contains 10001 elements
BEGIN array_map
END array_map: 0.0109

BEGIN foreach:
END foreach: 0.0052

[boombick@srv01 /tmp]$

Как видно из результата, array_map действительно проигрывает по скорости foreach

Система Orphus

 


 

Comments: 19

  1. bappoy February 19th, 2010 at 2:24 pm

    array_map и иже с ним показывает меньшую производительность, чем простой foreach, поэтому я их не использую. Но от foreach тоже корёжит.

  2. boombick February 19th, 2010 at 2:26 pm

    Хм.. Произведу замеры :)

  3. adw0rd February 19th, 2010 at 4:00 pm


    $square = function($number) {
    return $number * $number;
    }; // <----

    Теперь точка запятой обязательное требование в PHP? Или это какой-то стиль кодирования?

  4. can3p February 19th, 2010 at 5:56 pm

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

    А как реализованы анонимные функции в php? Есть контексты исполнения вроде тех же, что в js?

  5. boombick February 19th, 2010 at 7:15 pm

    http://ru.php.net/manual/en/functions.anonymous.php – use official docs, Luke )))

  6. boombick February 19th, 2010 at 10:51 pm

    2adw0rd
    А что не так? По-моему, точка с запятой всегда ставилась после объявления переменной. Более того, ее отсутствие – это синтаксическая ошибка.

  7. qnikst February 20th, 2010 at 4:52 pm

    Я не считаю, что хорошо разбираюсь в пхп, но всё же напишу своё скромное мнение.

    Меня всегда интересовал вопрос, зачем использовать более сложные методы, где сама логика подсказывает другое решение.
    Если у вас обычный массив с числовым индексом, то решения лучше, чем
    $n = sizeof($arr);
    for ($i=0;$i<$n;$i++){}
    не найти.

    В то время как foreach необходим для обхода хэш мапы, foreach представляет тот же самый проход итератором по hashmap’e как и в java, c# и прочих языках, где внутренняя структура данных не спрятана от программиста.

    Использование данных возможностей по назначению, принесёт 1). большой прирост производительности, 2). логику понимания происходящего.

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

  8. DimoNya February 20th, 2010 at 9:20 pm

    qnikst, никогда не задумывался, Ваши слова задели и поучили! весьма признателен за Ваши пояснения =) В догунку Ваших слов:

    Array contains 100001 elements
    time to for: 0.2365
    time to array_map: 0.4623
    time to foreach: 0.3393

  9. kost BebiX February 20th, 2010 at 10:06 pm

    Ну, вообще-то при серьезных объемах данных и прочем просто foreach использовать нельзя, ибо памяти не хватит (если делать операции над итератором, который просто по одному вытягивает данные), а во-вторых ведь способ с map можно распараллелить на много (map-reduce), тогда он будет быстрее.

  10. kost BebiX February 20th, 2010 at 10:12 pm

    и да, перелазьте на питон пока не поздно, честное слово получите удовольствие начав писать на красивом и правильном языке

    $square = function($number) {
    return $number * $number;
    };
    $squared = array_map($square, $primeNumbers);

    превратится в

    squared = map(lambda x: x*x, prime_numbers)

    (еще вариант):

    squared = [x*x for x in prime_numbers]

  11. Rett Pop February 22nd, 2010 at 8:56 am

    >Я, честно говоря, был слегка удивлен.
    А чему удивляться? Или для каждого элемента вызывать функцию (включая все накладные расходы по работе со стеком, параметрами и прочим), или передать ссылку на значение? Конечно первое будет дольше…

  12. boombick February 24th, 2010 at 11:23 am

    2kost BebiX:
    Да и на пайтоне тоже пишем :) Красивее и приятнее – несомненно! Но и пхп имеет свою нишу, да и просто захотелось показать некоторые возможности новой версии PHP.

  13. boombick February 24th, 2010 at 11:24 am

    2qnikst:
    Спасибо за отличный комментарий!

  14. mrl March 9th, 2010 at 10:02 pm

    Шутка про четные простые числа – зачетная.

  15. boombick March 9th, 2010 at 10:32 pm

    mrl, вот блин :))) Только что осознал. что действительно странновато :))))

  16. Serge March 12th, 2010 at 10:12 am

    А чем 2 не простое число?

  17. Alex March 15th, 2010 at 1:59 pm

    Многие постоянно твердят про огромный расход памяти для foreach при простых проходах, низкую скорость копирования массива для прохода, невозможность менять элементы массива в проходе, блаблабла.

    И почему-то все забывают про одну замечательную конструкцию:

    foreach ($bigbigbig_array as $key => &$value)
    {
    # your code here (you can even modify $value and it’ll get into original array)
    } unset($value);

    unset нужен для того, чтобы после при $value = ‘mydata’; не получить случайное изменение последнего элемента массива по ссылке в $value

  18. Егор March 15th, 2010 at 4:17 pm

    Полностью согласен с Alex-ом.
    А что касается замены foreach – так извините, даже мануал с дефолтными примерами на массивы на php.net – этим оператором решается!

  19. san April 29th, 2010 at 12:41 pm

    kost BebiX,
    мнея Perl устраивает
    @array = 0..1000; @squared = map{$_*$_} @array; print join” “,@array;
    @array = 0..1000; push @squared, $_*$_ for @array; print join” “,@array;
    @array = 0..1000; $_*$_ for @array; print join” “,@array;

Add a Comment