PHPのarray_randは値でなくキーを返し、num指定で戻り値の型が変わる

array_randは、配列からランダムに選んだ「キー」を返す関数です。値ではないうえ、第2引数が1のときは文字列かint、2以上のときは配列と、戻り値の型が変わります。

「ランダムに1件取りたいだけ」のつもりで書いて、画面に配列の添字が出てきた、という経験はないでしょうか。私は何度かやりました。名前に「rand」とあるので、つい値が返ると思い込んでしまうんですよね。この記事では、array_randで引っかかりやすい点を、実際に動かした結果をもとに整理します。

array_randは値ではなくキーを返す

まず基本から。戻り値は要素の値ではなく、選ばれた要素のキーです。連想配列でも連番の配列でも同じです。値が欲しいときは、キーで配列を引き直します。

<?php
$fruits = ['x' => 'apple', 'y' => 'banana', 'z' => 'cherry'];

$key = array_rand($fruits);
echo $key;            // x, y, z のどれか(値ではなくキー)
echo $fruits[$key];   // apple, banana, cherry のどれか

連番の配列だと、キーが0, 1, 2…なのでintが返ってきます。そのまま表示すると「3」のような数字だけが出て、一瞬なにが起きたのか分からなくなります。引き直す一手間を挟めば済む話ですが、知らないと地味に時間を取られます。

第2引数で戻り値の型が変わるのはなぜ?

array_randの第2引数は取り出す件数です。省略または1なら、キーが1つ(文字列かint)返ります。2以上なら、キーの配列が返ります。ここが一番のハマりどころで、件数を変数にしている場合、1のときだけ型が変わります。

<?php
$fruits = ['x' => 'apple', 'y' => 'banana', 'z' => 'cherry'];

var_dump(gettype(array_rand($fruits)));      // string
var_dump(gettype(array_rand($fruits, 1)));   // string(1を渡しても配列にはならない)
var_dump(gettype(array_rand($fruits, 2)));   // array

たとえば「おすすめをN件表示する」処理でNを設定値にしておくと、N=1の日だけforeachが落ちます。件数が可変なら、(array)でキャストして常に配列にそろえておくのが安全です。

<?php
$n = 1; // 設定値。1でも2以上でも同じ形で扱いたい
$keys = (array) array_rand($fruits, $n);
$picked = array_map(fn ($k) => $fruits[$k], $keys);

これで件数がいくつでも、値の配列として同じように回せます。

空配列や件数オーバーはValueErrorになる

PHP 8では、空配列を渡したときや、件数が1未満・要素数より大きいときにValueErrorが投げられます。実際に動かすと、メッセージは次のとおりでした。

<?php
try { array_rand([]); } catch (\ValueError $e) { echo $e->getMessage(); }
// array_rand(): Argument #1 ($array) cannot be empty

try { array_rand(['a', 'b'], 3); } catch (\ValueError $e) { echo $e->getMessage(); }
// array_rand(): Argument #2 ($num) must be between 1 and the number of elements in argument #1 ($array)

検索結果の絞り込み後など、空になりうる配列に使うなら、呼ぶ前に件数を確認するか、min($n, count($arr))で上限を丸めておくのが無難です。例外で受ける手もありますが、「空は普通に起こる」場面なら事前チェックのほうが読みやすいと思います。

複数件を取ると順番はシャッフルされない

もうひとつ見落としやすいのが順序です。複数件を取ったとき、返るキーは元の配列での並び順のままです。ランダムなのは「どれを選ぶか」だけで、「どの順で返すか」ではありません。

<?php
$keys = array_rand($fruits, 2);
// 毎回 ["x","y"] / ["y","z"] / ["x","z"] のどれか
// ["z","x"] のように逆順になることはない

おみくじの順位や出題順のように、選んだあとの並びもランダムにしたいなら、取り出したあとにshuffleをかけます。

<?php
$picked = array_map(fn ($k) => $fruits[$k], (array) array_rand($fruits, 2));
shuffle($picked);

これで「選び方」と「並び順」の両方がランダムになります。

shuffleはキーを捨てて振り直す

配列全体を混ぜたいときはshuffleを使いますが、これは引数の配列を直接書き換える関数で、キーは保持されず0からの連番に振り直されます。連想配列に使うと、キーの情報が消えます。

<?php
$c = ['x' => 1, 'y' => 2];
shuffle($c);
echo json_encode($c);  // [2,1] のように、キー x, y が消えて連番になる

キーを残して混ぜたい場合は、キーだけを取り出して混ぜ、その順で組み直します。

<?php
$keys = array_keys($fruits);
shuffle($keys);

$shuffled = [];
foreach ($keys as $k) {
    $shuffled[$k] = $fruits[$k];
}
// 例: {"z":"cherry","x":"apple","y":"banana"}

元の配列を壊したくないときは、先にコピーを作ってからshuffleするのも忘れずに。引数が参照渡しなので、元の配列がその場で変わってしまいます。

セキュリティ用途ならRandomizerを使う

array_randやshuffleは、PHPの通常の乱数(メルセンヌ・ツイスタ)をもとにしていて、暗号論的に安全な乱数ではありません。mt_srandでシードを固定すれば結果も再現できてしまいます。抽選の公平さが問題になる場面や、トークン用途には向きません。

PHP 8.2以降では、Random\Randomizerが使えます。デフォルトのエンジンは暗号論的に安全なものなので、そのまま使えます。

<?php
$r = new Random\Randomizer();

$keys = $r->pickArrayKeys($fruits, 2);   // キーの配列。1件でも必ず配列で返る
$mixed = $r->shuffleArray(['a', 'b', 'c']); // 元の配列は変更せず、混ぜた新しい配列を返す

pickArrayKeysは件数が1でも常に配列で返るので、先ほどの型が変わる問題も起きません。shuffleArrayも元の配列を壊しません。新しく書くコードで、PHPが8.2以上と分かっているなら、こちらを選ぶ手はありそうです。

まとめ

array_randは「値ではなくキー」「件数1のときだけ型が変わる」「複数件でも順序はシャッフルされない」の3点を覚えておけば、だいたいのハマりは避けられます。件数が可変ならキャスト、空になりうるなら事前チェック、キーを残したいならキーだけ混ぜる、という対処です。安全性が絡む場面では、Randomizerに乗り換えるのがいいのかもしれません。名前に似合わず癖のある関数だなあ、という気がしています。

よくある質問

Q. array_randで値を直接取得できますか?
A. できません。返るのはキーなので、$array[$key]で値を引き直します。複数件なら、array_mapでキーから値の配列に変換できます。

Q. array_randは同じ要素が重複して選ばれることはありますか?
A. 1回の呼び出しの中では、同じ要素が複数回選ばれることはありません。ただし、呼び出しを繰り返せば同じ要素が出ることはあります。

Q. 抽選や認証用のランダム選択にarray_randを使ってよいですか?
A. 暗号論的に安全な乱数ではないため、公平性やセキュリティが重要な場面には向きません。PHP 8.2以降ならRandom\RandomizerのpickArrayKeysやshuffleArrayを使うほうが安心です。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です