PHP
array
unset

PHPのarrayは単純配列に見えることもあるけど実は常に連想配列

色々な人が色々な所で書いてるけど、割りと本質的なところは気にしてなかったりするのだなあ。

連想配列ってまあまあ便利だよね

PHPには連想配列というまあまあ便利機能がある。
機能自体はご存知かつ、活用してる人が多いだろう。
これは歴史的にはPerlから連綿と受け継いだものだ。

PHP: Arrays - Manual

<?php
$array = array(
    "foo" => "bar",
    "bar" => "foo",
);

// as of PHP 5.4
$array = [
    "foo" => "bar",
    "bar" => "foo",
];
?>

一方、連想でない配列もある。仮に通常配列と呼ぼうか。

<?php
$array = array(
    "bar",
    "foo",
);

// as of PHP 5.4
$array = [
    "bar",
    "foo",
];
?>

何の話かというと、キーの設定方法であり、キーの参照方法が違う。

  • 前者=連想配列はキーに文字列を使い、
  • 後者=通常配列はキーに整数の0からの連続値を使う。

と言うことになってる。一般的な言語ではこれらは大概別物の言語機能だ。

例えばunset()で変なことになる

例えば、stackoverflowの次の質問。

php - How to Remove Array Element and Then Re-Index Array? - Stack Overflow

配列を造って、0番目の要素を削除したら、欠番が出来てしまうんだが、という文脈。

whatever.php
$foo = array(

    'whatever', // [0]
    'foo', // [1]
    'bar' // [2]

);

質問のソースはこれで終わってしまってるが、ベテランPHPプログラマであれば、あーフンフン、これね。そうだねー、って話なのだ。

先のソースには暗黙的にこれが続いてる。

whatever.php
unset($array[0]);
var_dump($array)

欠番って何?

実はPHPは、設定方法の如何に関わらず、必ず連想配列が出来上がる。前者の通常配列であってもだ。このキモい言語機能により、PHPの配列は、整数キーと文字列キーの混在を認めており、しかも保存順序を保証している。

混在の例として、オフィシャルマニュアルには、次のような例があげられている。

PHP: Arrays - Manual

example_1_simplearray.php
<?php
$array = array(
    "foo" => "bar",
    "bar" => "foo",
    100   => -100,
    -100  => 100,
);
var_dump($array);

また、内部構造の解説 PHP: 配列の扱い - Manual で、配列はHashTable構造体に入れると断言している。

つまり先の例 whatever.php の unset($array[0])は

  • 0番目の要素を削除してるわけではなく
  • キー[0]の要素を削除しているのだ

それでは通常配列の初期化は何だったのか

  • PHPで通常配列の初期化に見える構文は、
  • 実は連想配列インデックスの省略であり、
  • 数字の0からのナンバリングを勝手に行う。
    • より厳密には直前の整数インデックスの次の値を使う

故に、通常配列に統一して扱う限りは、とくに意識せずに「通常配列として」使えるようになってる。

json_encode() での小細工

  • PHPにはjson_encodeするときに単純配列っぽかったら単純配列っぽい出力をする機能が入っている。
  • 事実、json_encodeには、そのための判定オプションが付いている PHP: json_encode - Manual 2番めの引数のことだ。
  • 例えば今流行りのUnity3dとJSONで通信しようとしたときに非常に困る。

先頭の例に戻ると

["whatever", "foo", "bar"]

これが [0] を削除した途端、

{"1":"foo", "2": "bar"}

こうなってしまうのだ。
JSONの表記として、キー値の数値は認めてないため、JSON Objects表記を適用した、ということなのだろう。
Unity3dのC#としては両者は大違いである。

  • 前者はList<>で
  • 後者はDictionary<>かも知れないしclassかも知れないしstructかも知れない

必然、List<>で待ってるところに{}が来ても困るわけだ。入れる場所がない。

何故こんな大きなお世話の挙動をするのか?

先のオフィシャルでは別の例があげられている。

example2_typing_and_overwriting.php
<?php
$array = array(
    1    => "a",
    "1"  => "b",
    1.5  => "c",
    true => "d",
);
var_dump($array);
?>

見るからにキモいが、これは驚くべきことに、全てのキー値がintval()される。
つまり、全てのキー値が1にしかならないため、最終的に $arrayの中身は "d" しか残らない。

この話が出て来ると「連想配列になるんじゃねーのかよ」と思うだろう。ソーデスネ。筆者もこれ書きながら困っていたところですよ。まあ理由はサブタイトルの通り、type casting and overwritingなのだが、初期化構文なのに勝手にoverwritingしてんじゃねーよって話である。

ここからは筆者の想像だが、多分これは話の順番が違う。

  • $_REQUEST[]等から入力した値を、そのまま数値計算させてやりたい、みたいなのがあったのだろう。つまり内部的に、数値に見える箇所は、そのまま極力数値として解釈しよう、という忖度があちこちにある。
  • 一方、通常配列と連想配列の使い分けは、初心者には混乱する可能性がある。故に意識せずとも使えるようにしてやりたい。

この無関係の2件をマージして、結果的に出来上がった言語機能が、問題のPHP独自の連想配列ということ。なのではないかなあ。

PHPの連想配列については、これに限らずキモい挙動がかなりあるので、オフィシャルを熟読することを進める。