Unknown::Programming このページをアンテナに追加 RSSフィード

2009-11-12 久方

配列か連想配列かを判別する

PHPにおいて、ある配列連想配列として使われているのかどうかを判別したい場合どうすればいいのか。

色々調べてたらこんな記事を発見しました。

<?php

function is_hash(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}
テレパス・ラボ : 連想配列 判定関数%28is_hash%29

なるほど。

array_keysで配列のキー一覧を取得し、そのキー一覧が0からの連番になっているかどうかを比較すれば、対象の配列配列なのか連想配列なのかを判別できるという理屈ですね。

確かにこれで判別ができそうです。

ですがひとつだけ気になる部分があって、array_keysやrangeを使って一時的な配列を確保しているのは無駄が多いのではないかと。

対象となる配列が大きければ大きいほどメモリ効率が悪くなりそうな気がします。

要件としては、対象の配列のキーが0からの連番になってるかどうかを調べたいだけなのでforeachでループしながら比較する方が効率良いのではないかと思い、実装してみました。

<?php

function is_hash(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

こんな感じですね。

そして次にベンチマークをとってみます。

<?php
// PHP5.2.8

require_once "Benchmark/Iterate.php";

function is_hash1(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}

function is_hash2(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

$range = range(1,1000);  // 実験回数1000回
$array1 = range(0,1000); // 要素1001の配列

function is_hash_a () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash1($array1);
    }
}

function is_hash_b () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash2($array1);
    }
}

$bench   =& new Benchmark_Iterate;
$bench->run(1,'is_hash_a'); $result1 = $bench->get();
$bench->run(1,'is_hash_b'); $result2 = $bench->get();

print('is_hash_a -> '.$result1['mean'] . "\n");
print('is_hash_b -> '.$result2['mean'] . "\n");

要素が1001個ある配列を1000回繰り返し実行した場合のベンチマークです。

$ php bench.php
is_hash_a -> 0.265428066254
is_hash_b -> 0.106162071228

foreachで処理する方が2.6倍ほど早くなりました。

他にも色々な条件でやってみます。

// 要素が10個の配列を10万回実行
$ php bench.php
is_hash_a -> 0.319720983505
is_hash_b -> 0.169075965881

要素が少ない場合でもforeachの方が早いですね。

// 要素が30万個の配列を1回実行
$ php bench.php
is_hash_a -> 0.308174133301
is_hash_b -> 0.038535118103

要素が多い場合は速度の差が顕著にでます。

因みに配列の要素を100万個にしてみたら

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 524288 bytes)

というエラーがでました。どうやらarray_keysやrangeで確保するメモリの量が多すぎてプログラムが落ちるようです・・・。foreachなら100万個でも難なく処理できます。

で次に連想配列を渡したときのベンチマークですが、foreachの場合、キーが連番じゃなかったり数字じゃなかった時点で処理が終了するのでめちゃめちゃ早くなりました。

試しに文字列のキーを持つ10万要素の連想配列ベンチマークを取ってみます。

// 以下のような連想配列でベンチマークしてみる
// $array1 = array_merge(array('a'=>1),range(0,100000));
$ php bench.php
is_hash_a -> 0.933251142502
is_hash_b -> 1.59740447998E-5

比較にならないぐらい桁違いに早いですね。数万倍くらい?

結論としては

  • 配列の大きさに関わらずforeachの方が早い
  • 配列が大きければ大きいほどforeachの方が更に早い
  • 対象の配列連想配列だった場合、foreachの方が桁違いに早い。

という感じなりますか。

foreach万歳ですね。ただひとつだけデメリットをあげるならば、ワンライナーじゃないというところになりますか^^;

さてこれでカンペキ!と言いたいところですが、実は実際のところ、連想配列として利用する配列のキー名に0という数字は使わないという前提条件がある場合、その対象配列の最初のキーが0かどうかだけをチェックすればそれだけでOKだったりするんですよね。

<?php

function is_hash_lite(&$array) {
    reset($array);
    list($k) = each($array);
    return $k !== 0;
}

こんな感じ。もちろん速度的にはピカ一で早いです。

厳密にチェックしたい場合はforeachを使い、前提条件がある場合は最初のキーだけをチェックする感じで良いんじゃないでしょうか。

DQNEODQNEO 2009/12/04 23:47 これは良い記事ですね。大変参考になりました。

andoando 2014/11/07 12:49 むしろこっちの方が正解だと思う。
配列のキーが0から始まっていない時点で、それは連想配列だからだ。
function is_hash(array $array)
{
foreach($array as $key => $value) return $key !== 0;
}

onoono 2015/01/09 12:13 些細なことですが、is_hash_liteの list($k) = each($array); は $k = key($array); の方が素直な気がします^^

私はさらに簡易に isset($array[0]) で判定したりもします。
もちろんこれでは万全ではないのですが、これで事足りる場合も少なくないので。

ちなみに、is_hashの方は下の実装もありかもですね。

function is_hash(&$array) {
for($i=0,$len=count($array);$i<$len;$i++) {
if (!isset($array[$i])) return true;
}
return false;
}

多くの場合はこちらの方が速いですが、単純配列の特定キーの要素をunsetした場合にはオリジナルのforeach版の方が速いと思います。

どれを判定に用いるかは「ケースによりけり」ということになると思います。

fbisfbis 2015/01/09 15:46 onoさん
コメントありがとうございます。

issetを使ったis_hashですが、これだと添え字の順序がおかしい場合でも配列として判断されますね

$a = [
'2' => 1,
'0' => 2,
'1' => 3,
];
var_dump(is_hash($a)); // bool(false)

配列かどうかを可能な限り厳密にチェックしようと思ったら、結局添え字を順番に取得していって連番と比較するしか方法がないのかなぁと思います。

fbisfbis 2015/01/09 15:51 andoさん、コメントありがとうございます。

それだと

$a = [
'0' => 1,
'aaaa' => 2,
];

が連想配列にならないのでダメですね。

onoono 2015/01/13 13:04 fbisさん

> issetを使ったis_hashですが、これだと添え字の順序がおかしい場合でも配列として判断されますね

ええ。あくまでも特定条件下における簡易判定です^^

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証