hnwの日記

PHPの配列のキーについて調べてみる

先日書いた「PHPの奇妙なround関数」は、重箱の隅をつつくような内容の割には注目を頂いたようで何よりです。ブックマーク数が増えていくのを見るのはとても楽しい経験でした。気をよくして他の持ちネタも披露してみます。今回はPHPの配列のキーに関して簡単に紹介した上で、関連してバグなのか仕様なのかわからない挙動を指摘します。


まず、PHPの配列について簡単に紹介します。PHPには配列と連想配列の区別がありません。これは他の言語ではあまり見られない特徴だと思います。PHPのarrayはいわゆる配列と連想配列の両方の性質を持っていますが、他の言語で言うと連想配列で実現されていると言えます。この記事ではPHPの流儀で「配列」と呼びますが、他の言語のユーザーにとっては連想配列と読み替えた方が自然かもしれません。連想配列なのに代入された順番を覚えていたりするので、結局違和感があるかもしれませんが…。


PHPの配列の実装についてはソースコードを読んでもいいと思いますが、小泉さんの書かれた図(ハッシュテーブルの実装)を見れば想像がつくのではないでしょうか。

また、本家マニュアルの配列の説明にも案外色々と書いてあります。


さて、PHPの配列のキーとしては整数と文字列のどちらかが利用できます。それ以外の型のキーは持てません。他の型をキーに使おうとした場合には整数か文字列かのどちらかに型キャストされます。


文字列が指定された場合でも、キーに10進数として解釈できる文字列が指定された場合は、文字列から数値にキャストされます(プログラマの型への無知から来る混乱を防ぐための正規化なのでしょうか?)。例えば下記のようになります。

$ php -r '$a=array(); $k1="123"; $k2=123; $a[$k1]=1; $a[$k2]=2; var_dump($k1,$k2,$a);'
string(3) "123"
int(123)
array(1) {
  [123]=>
  int(2)
}

文字列のつもりだったキー"123"は整数123とみなされて格納されていることがわかります。
このような数字列を文字列のまま配列のキーとして使うことはできません。


ただし、10進数に見える文字列であっても符号付き32ビット整数*1の範囲外だった場合には文字列のままキーとして使われます。

$ php -r '$a=array(); $k1="1000000000"; $k2="10000000000"; $a[$k1]=1; $a[$k2]=2; var_dump($k1,$k2,$a);'
string(10) "1000000000"
string(11) "10000000000"
array(2) {
  [1000000000]=>
  int(1)
  ["10000000000"]=>
  int(2)
}

ここまででも他の言語の感覚で言うと気持ちが悪いと思いますが、実は整数の最小値および最大値のところで奇妙な挙動を示します。

$ php -r '$a=array(); $k1=0x7ffffffe; $k2="$k1"; $a[$k1]=1; $a[$k2]=2; var_dump($k1,$k2,$a);'
int(2147483646)
string(10) "2147483646"
array(1) {
  [2147483646]=>
  int(2)
}
$ php -r '$a=array(); $k1=0x7fffffff; $k2="$k1"; $a[$k1]=1; $a[$k2]=2; var_dump($k1,$k2,$a);'
int(2147483647)
string(10) "2147483647"
array(2) {
  [2147483647]=>
  int(1)
  ["2147483647"]=>
  int(2)
}

このように、符号付き32bit整数の最大値である0x7fffffffでは文字列から整数への正規化がされず、数値と文字列と2種類のキーが共存しています。最小値の-0x80000000についても同様です。ありがちな境界値バグのようにも思いますが、本当にバグかどうかはわかりません。バグだとしても実害は殆ど無いだろうと個人的には考えています。


ちなみに、連想配列のキーとして浮動小数点数を指定した場合には整数に型変換されます。小数点以下は0方向に切り捨てられます。

$ php -r '$a=array(); $k1=1.99; $k2=-$k1; $a[$k1]=1; $a[$k2]=2; var_dump($k1,$k2,$a);'
float(1.99)
float(-1.99)
array(2) {
  [1]=>
  int(1)
  [-1]=>
  int(2)
}

また、整数におさまらない範囲の浮動小数点数を指定した場合には符号付き32bit整数の最小値、-0x80000000になるようです。

$ php -r '$a=array(); $k1=0x80000000; $k2=$k1+1; $k3=-$k2; $a[$k1]=1; $a[$k2]=2; $a[$k3]=3; var_dump($k1,$k2,$k3,$a);'
float(2147483648)
float(2147483649)
float(-2147483649)
array(1) {
  [-2147483648]=>
  int(3)
}

これはCのソースコード中で何の手当てもせず、受け取ったキーを単純にdoubleからlongにキャストしているための挙動です(0方向への切り捨ても同じ理由ですね)。longの範囲外のdouble値をlongにキャストした場合の挙動は(少なくともC99では)未定義のようですから、どんなデタラメな値になっても文句は言えないように思います。少なくとも僕の手元の環境ではdoubleからlongにキャストする際にlongの範囲外だとLONG_MINになるようです。これもまず実害は無いかと思いますけど、本来ならWarningを出すべき場所のような気がします。


記事の内容をまとめると下記のようになります。

  • PHPの配列は他の言語で言う連想配列に近いものです。ですが配列のように順序も覚えています。
  • 配列ではキーとして整数か文字列が利用できます。他の型が指定された場合は整数か文字列のどちらかにキャストされます。
  • 配列のキーとして文字列が指定された場合でも、整数の範囲内でかつ10進数値として正しい文字列の場合には整数に型キャストされた上で格納されます。
  • 配列のキーとして浮動小数点数が指定された場合には整数に型キャストされます。
  • 配列のキーに関して腑に落ちない挙動を示す場合を2パターン指摘しました。
    • キーが文字列で"-2147483648"の場合と"2147483647"の場合
    • キーが浮動小数点数で-2147483648.999…〜2147483647.999…の範囲に無い場合


さて、いかがでしたでしょうか。今回は淡々とPHPの配列のキーについて紹介してみました。腑に落ちない点の2件以外は全部マニュアルに書いてあることなんですが、普段PHPを使っていても案外知らない人がいるのではないでしょうか。たまにはマニュアルを読み返してみて、ソースコードを流し読みしてみて、なんてのも楽しいもんですよ。ちなみに今回読んだのはZend/zend_hash.h中のマクロHANDLE_NUMERICあたりです。


ご意見・ご感想・タレコミなど何でもお待ちしております。

*1:プラットフォーム次第かと思いますけど、この記事では32ビットということにします