Help us understand the problem. What is going on with this article?

foreach, array_map, array_walkの比較

More than 1 year has passed since last update.

PHPのarray_map()array_walk()は引数順序の一貫性のなさが槍玉に上げられがちなので、せっかくなのでforeachを含めて、まとめて解説します。

はじめに三行で

  • まあforeachで書くのが簡単じゃね
  • array_walk_recursive()foreachには手の届かないことができるかもね
  • tadsanのpixivFANBOXもよろしく

早見表

関数/文 操作対象 特徴
array_map() 複数の配列 要素にコールバック関数を適用した結果をまとめた配列を返す
array_walk() 一つの配列変数のリファレンスの要素 コールバック関数から要素ごとのリファレンスを直接操作することができる
array_walk_recursive() 一つの配列変数のリファレンスの子要素 配列に含まれる配列の子要素も含めてリファレンスを直接操作することができる
foreach 一つのイテレータまたは配列(または配列の要素のリファレンス) 配列以外のイテレータも走査できる。文なので結果は返さないが、配列のキーまたはリファレンスを使って要素を変更することができる

今回は細かく説明しませんが、「配列」と「配列変数のリファレンス」には微妙な差があります。

array_map()array_walk()の差がわかりにくいケース

この例を見ると、array_map(), array_walk(), foreachのどれを使っても大差ないように見えます。

<?php

$ss = ["apple", "banana", "orange"];


// array_map()
echo "<ul>", PHP_EOL;

array_map(function ($s) {
    echo "    <li>", h($s), "</li>", PHP_EOL;
}, $ss);

echo "</ul>", PHP_EOL, PHP_EOL;



// array_walk()
echo "<ul>", PHP_EOL;

array_walk($ss, function ($s) {
    echo "    <li>", h($s), "</li>", PHP_EOL;
});

echo "</ul>", PHP_EOL, PHP_EOL;



// foreach
echo "<ul>", PHP_EOL;

foreach ($ss as $s) {
    echo "    <li>", h($s), "</li>", PHP_EOL;
}

echo "</ul>", PHP_EOL, PHP_EOL;

どれで書いても差がありません。この例においては、functionを書く必要がないforeachがもっとも簡潔だと言って支障ないですね。

改めて関数の説明

array_map()

array_map — 指定した配列の要素にコールバック関数を適用する

array array_map ( callable $callback , array $array1 [, array $... ] )

array_map() は、array1 の各要素に callback 関数を適用した後、 その全ての要素を含む配列を返します。 callback 関数が受け付けるパラメータの数は、 array_map() に渡される配列の数に一致している必要があります。

(PHP: array_map - Manualより抜萃、2018年6月24日閲覧)

ここで新たな用語「適用」(英語ではapply)が登場しました。非常にざっくりと説明すると、単に関数を呼び出して式を評価することです。

ここでは算数で出てきた式 $f(a)$ を「$a$に$f$を適用する」と読むことにします。$a$が「適用される」側です。 $g(x) = 2x$ としたとき、「$3$に$g$を適用すると$6$になる($g(3) = 6$)」です。

PHPでの実用的な例を出します。以下のような果物と値段の対応表を用意します。

$fruits = [
    'apple' => 100,
    'orange' => 150,
    'strawberry' => 80,
];

次に、日本の消費税を計算する関数 tax を用意します。

function tax($n): float
{
    return $n * 1.08;
}

これを使って果物それぞれの消費税込みの値段を知るには、$fruitsのそれぞれにtuxを適用する式を以下のように書けばよいですね。

var_dump(tax($fruits['apple'])); // => float(108)
var_dump(tax($fruits['orange'])); // => float(162)
var_dump(tax($fruits['strawberry'])); // => float(86.4)

これはこれでよいのですが、果物の種類が増えるごとに一行書き足していかなくてはいけなくなります。せっかく一個の配列 $fruits にまとまってるのですから、一気に計算したくなるのが人情です。

ところで、小学校や中学校の算数で、グラフを描く準備として数字の表を書いた覚えはありませんか? さきほどの $g(x) = 2x$ を $y = 2x$ と直せば、以下のような表を書いたおぼえがあります。

$x$ 0 1 2 3 4 5 6 7 8 9 10
$y$ 0 2 4 6 8 10 12 14 16 18 20

ここで再び array_map() の定義を持ち出します。

array_map — 指定した配列の要素にコールバック関数を適用する

配列の中に含まれる一個一個のことを、プログラミング言語では「要素」と呼びます。これは高校の算数の教科書で習った気がする「」と同じく英語のelementと対応することばで、単に分野によって定訳が異なるってだけの話です。

ことばの由来は算数の用語ですが、そんなに大事なことではないのでリンク先を読む必要は特にありません。ただのトリビアです。ただ、そのほかのパソコン用語でもelementに対応する用語はいろいろある1ので、ややこしいですね。

さて、array_map()のイメージは、$x$の列を「まとめて」$y$の列に変換することです。改めて上の処理を array_map() を使って書き直してみます。

var_dump(array_map(function($n){ return tax($n);}, $fruits));
// array(3) {
//   ["apple"]=>
//   float(108)
//   ["orange"]=>
//   float(162)
//   ["strawberry"]=>
//   float(86.4)
// }

この式は、以下のように書いてもまったく同じことです。

var_dump(array_map('tax', $fruits));
var_dump(array_map(function($n){ return $n * 1.08; }, $fruits));

ここで気をつけてほしいのは、array_map()は各要素に関数を適用した配列を返しますが、もとの配列変数$fruitは何の変化がない状態のままであることです。。


さらに、 foreach を使って書き直すと次のようになります。

$result = [];
foreach ($fruits as $name => $price) {
    $result[$name] = tax($price);
}

var_dump($result);

要は、この foreach を1行で書くためにあるのが array_map() だと思ってください。

最後に配列を組み立てるのがarray_map()の本質的な機能です。つまり、この記事の最初にサンプルコードとして上げたコードをarray_map()で動かすのは、基本的に無意味です。

// array_map()
echo "<ul>", PHP_EOL;

array_map(function ($s) {
    echo "    <li>", h($s), "</li>", PHP_EOL;
}, $ss);

echo "</ul>", PHP_EOL, PHP_EOL;

array_map()で呼ばれるコールバックは何も値を返してないですし、返り値を何にも使ってないですからね。

array_walk()

array_walk — 配列の全ての要素にユーザー定義の関数を適用する

bool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )

array 配列の各要素にユーザー定義関数 callback を適用します。

array_walk() は array の内部配列ポインタに影響されません。array_walk() はポインタの位置に関わらず配列の全てに渡って適用されます。

今度もまた「適用」の用語が出てきましたが、これはarray_map()とはすこし趣きがことなるものです。

比較のために、もういちどarray_map()の定義を抜萃します。

array array_map ( callable $callback , array $array1 [, array $... ] )

(PHP: array_walk - Manualより抜萃、2018年6月24日閲覧)

差がわかりましたか?

  • 引数の仕様が異なる
    • array_map()は1つ以上の任意個数の配列(array $array1 [, array $... ])を受け取る
    • array_walk()は配列変数のリファレンス(array &$array)をひとつだけ受け取る
  • コールバック関数の仕様が異なる
    • array_map()は、array_map()の引数として受け取った array $array1 [, array $... ] のそれぞれの要素を第1引数, 第2引数, 第n引数として受け取る
    • array_walk()は、(&$item1, $key)を受け取り、array_walk()の第3引数を指定した場合のみコールバック関数の第3引数としても渡される
  • 返り値の仕様が異なる
    • array_map() はキーを維持したまま、適用結果を要素として持つ配列を返す
    • array_walk()bool(true) を返す2

以上のような差異から、array_walk()が提供する機能はarray_map()とは目指すものがまったくことなることがわかります。

本質的に大きくことなる特徴として、array_walk()は配列そのものではなく、配列変数と要素のリファレンス(参照&)を操作の対象とすることが挙げられます。

さきほどのarray_map()と同じように、果物の消費税率をarray_walk()で計算する例を挙げます。

<?php

$fruits = [
    'apple' => 100,
    'orange' => 150,
    'strawberry' => 80,
];

// コールバック関数に `&` を付けるのを忘れてはいけない
array_walk($fruits, function(&$n){ $n = tax($n); }));

var_dump($fruits);
// array(3) {
//   ["apple"]=>
//   float(108)
//   ["orange"]=>
//   float(162)
//   ["strawberry"]=>
//   float(86.4)
// }

さきほどarray_map()は元の配列変数を変化させないと書きましたが、今度のarray_walk()配列変数を変化させることができます

これはRubyのArray#mapArray#map!の関係とすこし似てますが、コールバック関数の仕様がことなるので同じように書くことはできないので注意が必要です。

array_map()用のコールバック関数はfunction($n){ return tax($n);}でしたが、今度はfunction(&$n){ $n = tax($n); }です。ややこしいですね。


こんどの例もforeachで書き直すことができます。

foreach ($fruits as $name => &$price) {
    $price = tax($price);
}
unset($price); // ← この unset は省略してはいけない

var_dump($fruits);

これをわざと変化させたくないときは、こうすることもできます。

$taxed_fruits = $fruits;

foreach ($taxed_fruits as $name => &$price) {
    $price = tax($price);
}
unset($price); // ← この unset は省略してはいけない

var_dump($taxed_fruits); // 変化後・税込みの値段
var_dump($fruits); // 変化前・税抜きの値段

が、わざわざリファレンス(&)を使ってこんな書きかたをするメリットは乏しいので、ふつうに書いたほうがいいですね……。

$taxed_fruits = [];
foreach ($fruits as $name => $price) {
    $taxed_fruits[$name] = tax($price);
}

var_dump($taxed_fruits); // 変化後・税込みの値段
var_dump($fruits); // 変化前・税抜きの値段

foreach

これまで紹介したarray_map()array_walk()が関数なのに対して、foreachはPHPの言語構文です。

foreach は、配列を反復処理するための便利な方法です。 foreach が使えるのは配列とオブジェクトだけであり、 別のデータ型や初期化前の変数に対して使うとエラーになります。 この構造には二種類の構文があります。

foreach (array_expression as $value)
    

foreach (array_expression as $key => $value)
    

最初の形式は、array_expressionで指定した配列に 関してループ処理を行います。各反復において現在の要素の値が $valueに代入され、内部配列ポインタが一つ前に進められます。(よって、次の反復では次の要素を見ることになります。)

2番目の形式は、さらに各反復で現在の要素のキーを変数$keyに代入します。

オブジェクトの反復処理をカスタマイズすることもできます。

(PHP: foreach - Manualより抜萃、2018年6月24日閲覧)

PHPの入門書などでは繰り返しのための構文としてwhileforが紹介されますが、現実のPHPコードで有限回数の繰り返しをする際に利用されるのは、圧倒的にforeachの方です。

ここまでさんざんforeachを使ってきたのでわざわざ使用方法を説明することはないかもしれません。

配列を走査する場合は、for文よりもforeach文が、圧倒的にべんりです。

$ary = ['りんご', 'ごりら', 'らっぱ'];

for ($i = 0; $i < count($ary); $i++ ) {
    var_dump($ary[$i]);
}

forで書く場合は対象の配列が0から始まる連番であること、途中に連番の抜けがある場合の処理などを考慮する必要があり面倒ですが、foreachでは基本的にその配慮は不要です。

$ary = ['りんご', 'ごりら', 'らっぱ'];

foreach ($ary as $elm) {
    var_dump($elm);
}

array_map()array_walk()を使って配列を走査する簡単なパターンはforeachで簡単に書けることは、これまで延々と説明した通りです。逆にforeachで書けるものがarray_map()array_walk()で書けないことは多数あります。

そう、array_map()array_walk()は名前の通り、配列にしか利用できません。一方、foreachは、それ以外のいろいろなものを走査できます。

筆者の見解としては、array_map()とかarray_walk()とか覚えるよりも素直にforeachで書くのがよいです。


おまけ1: foreach, array_map(), array_walk()の差が際立つ例

「配列の配列の要素をソートする関数」を例としてみると、foreach, array_map(), array_walk()はそれぞれ別の書きかたをする必要があります。

<?php

$as = [
    [3, 4, 2, 5, 1],
    ['p', 'h', 'p'],
    [5, 2, 1, 4, 3],
];

$x = sort_foreach($as);
$y = sort_array_map($as);
$z = sort_array_walk($as);

var_dump($x === $y && $x === $z && $y === $z);
//var_dump($x, $y, $z);

function sort_foreach(array $as)
{
    foreach ($as as $k => $a) {
        sort($a);
        $as[$k] = $a;
    }

    return $as;
}

function sort_array_map(array $as)
{
    return array_map(function ($a) {
        sort($a);
        return $a;
    }, $as);
}

function sort_array_walk(array $as)
{
    array_walk($as, function (&$a) {
        sort($a);
    });

    return $as;
}

ねむいので説明はしません。

おまけ2: array_walk_recursive()

foreachおよびarray_map(), array_walk()は配列の要素を走査しますが、array_walk_recursive()は配列を木構造とみなして葉まで再帰的に走査することができます。

これはarray_walk()の親戚なので、操作対象はarray_walk()と同じく配列変数と要素のリファレンスです。

ここで紹介するのはWebアプリケーションのエラーログにユーザーが入力したパスワードが残らないようにするための簡単な例です。

<?php

set_exception_handler(function($e) {
    $backtrace = filter_backtrace($e->getTrace());
    var_dump($backtrace);
    exit(1);
});

$input = [
    'username' => 'hogehoge',
    'password' => 'fugafuga'
];
login($input);

function login(array $input)
{
    $user = check_user($input['username'], $input['password']);
}

function check_user($username, $password)
{
    throw new \RuntimeException("未実装です");
}

/**
 * backtraceから引数に含まれるものを含めてpasswordをフィルタリングする
 */
function filter_backtrace($backtrace)
{
    foreach ($backtrace as $i => $b) {
        $ref = new ReflectionFunction($b['function']);
        foreach ($ref->getParameters() as $j => $arg) {
            if ($arg->name === 'password') {
                $backtrace[$i]['args'][$j] = '(password censored)';
            }
        }
    }

    array_walk_recursive($backtrace, function (&$val, $key) {
        if ($key === 'password') {
            $val = '(password censored)';
        }
    });

    return $backtrace;
}

残念ながら、この例はarray_walk_recursive()だけで完結させることはできません。

適当に書いただけだし、飽きてきたのでコードの詳しい説明はしません。本番運用するならもうちょっと考慮しないとエラー出るよ。

おまけ3: 関連記事

配列とかリファレンスとかについてはいままでも怪気炎を吐いてきたので、興味が残ってたら読んでね。


あとがき

この記事が役に立ったならtadsanのpixivFANBOXを購読してね。さもなければ今後のモチベーションの維持は保障できない

注釈


  1. HTMLで一般にタグと呼ばれるものもelement(日本語での定訳は「要素」)と呼ばれます。タグとは<a></a>のような目印のことで、 <a href="https://example.com/">ほげほげ</a> のような塊が「要素」(この場合はa要素)です。 

  2. マニュアルには実行に失敗した場合は false を返すと説明されてるのですが、どんな場合が失敗なのかよくわからないです。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした