php で配列やオブジェクトをアクセスキーにする
ArrayAccess を読みつつ色々試していて気づいたんですが、どうも $offset
引数があらゆる型を受け入れてくれるみたいです。
それでふと思い至って下記のクラスを作ってみました。
class Map implements \ArrayAccess, \Iterator, \Countable
{
private $keys = [];
private $vals = [];
private $position = 0;
private function getKey($offset)
{
foreach ($this->keys as $n => $key) {
// ここを書き換えれば同値判定を差し替えられる
if ($key === $offset) {
return $n;
}
}
return null;
}
public function offsetExists($offset)
{
$key = $this->getKey($offset);
return $key !== null;
}
public function offsetGet($offset)
{
$key = $this->getKey($offset);
if ($key !== null) {
return $this->vals[$key];
}
trigger_error("Undefined index: $offset");
}
public function offsetSet($offset, $value)
{
// 追加時の null もキー扱いとする
$key = $this->getKey($offset);
if ($key === null) {
$this->keys[] = $offset;
$this->vals[] = $value;
}
else {
$this->keys[$key] = $offset;
$this->vals[$key] = $value;
}
}
public function offsetUnset($offset)
{
$key = $this->getKey($offset);
if ($key !== null) {
unset($this->keys[$key]);
unset($this->vals[$key]);
}
}
public function current()
{
return $this->vals[$this->position];
}
public function next()
{
$this->position++;
}
public function key()
{
return $this->keys[$this->position];
}
public function valid()
{
return isset($this->keys[$this->position]);
}
public function rewind()
{
$this->position = 0;
// unset すると歯抜けができるはずなので連番振り直し
$this->keys = array_values($this->keys);
$this->vals = array_values($this->vals);
}
public function generate()
{
foreach ($this->keys as $n => $key) {
yield $key => $this->vals[$n];
}
}
public function count()
{
return count($this->keys);
}
}
試してみたら動いてしまいました。
下記のように使います。
$map = new Map();
echo "# ACCESS\n";
// 配列をキーにできる
$key = ['akey'];
$map[$key] = 'hoge';
printf("array key value: %s\n", $map[$key]);
// オブジェクトをキーにできる
$key = new stdClass();
$map[$key] = 'fuga';
printf("object key value: %s\n", $map[$key]);
// 文字列をキーにできる
$key = 'key';
$map[$key] = 'piyo';
printf("string key value: %s\n", $map[$key]);
echo "\n# FOREACH\n";
// foreach で回せる
foreach ($map as $k => $v) {
printf("key type: %s=%s\n", gettype($k), json_encode($k));
printf("val: %s\n", $v);
}
echo "\n# GENERATOR\n";
// generator で回せる
foreach ($map->generate() as $k => $v) {
printf("key type: %s=%s\n", gettype($k), json_encode($k));
printf("val: %s\n", $v);
}
echo "\n# COUNT\n";
// count を取れる
printf("element count: %s\n", count($map));
上の例の出力は以下となります。
# ACCESS
array key value: hoge
object key value: fuga
string key value: piyo
# FOREACH
key type: array=["akey"]
val: hoge
key type: object={}
val: fuga
key type: string="key"
val: piyo
# GENERATOR
key type: array=["akey"]
val: hoge
key type: object={}
val: fuga
key type: string="key"
val: piyo
# COUNT
element count: 3
配列やオブジェクトをキーとして利用したい状況はまれによくあるため、そういうときに使えるかもしれません。
ただし下記に注意です。
- ドキュメントに言及がない
- 現状「たまたまそうなっている」とも言えるので良い実装ではない
-
$map[] = 123;
で null が来るので多分それ関係?
- 線形探索になる
- 要素の数に比例して探索が遅くなる
- serialize とかしてキーにしてもいいが、それなら普通に配列を使えばいい
- 文字列と整数キーは別扱いになる
- getKey を修正すれば同一視できると思う
- キーは内部で保持されるため GC の対象にならない
- spl_object_hash に重複が無ければ…!
-
$map[] = 123;
で追加ではなく null キーになる- 多分両立できないので
offsetSet
を弄るしかないと思う
- 多分両立できないので
- phpstorm に激しく怒られる
- 「Illegal array key type」とかそういうの
一番上が割と致命的なので無理に ArrayAccess なんて使わないで普通にメソッドベースでいいと思うよ。