【PHP】組み込み関数spl_object_hashの返り値が衝突する話

  • 2023年5月9日
  • PHP

 spl_object_hash はオブジェクトを元にハッシュ値を取得する PHP 組み込みの関数です。公式マニュアルには次の説明があります。
PHP: spl_object_hash – Manual

spl_object_hash(object $object): string

この関数は、オブジェクトの一意な識別子を返します。この ID は、
オブジェクトを保存する際のハッシュのキーとして使えます。
また、オブジェクトが破棄されるまでは、オブジェクトを識別するための値としても使えます。
オブジェクトが破棄されると、そのハッシュが他のオブジェクトで再利用されてしまうことがあります。
この振る舞いは、spl_object_id() に似ています。

 一見オブジェクト毎に一意なハッシュ値を得られることを期待できそうな関数ですが、実は独特な挙動が含まれており注意が必要です。これは公式マニュアルに次の様にざっくり書かれています。

注意:

オブジェクトが破壊されるとき、そのハッシュが他のオブジェクトで再利用されてしまうことがあります。

 要するに spl_object_hash のハッシュがユニークな値であるか否かはオブジェクトがメモリ上に存在するか否かに依存しているということです。このため次のコードができあがります。

<?php

var_dump(
    // 変数として保持し続けるので期待通り異なるハッシュが返ってくる
    spl_object_hash($a = new stdClass()), // string(32) "00000000000000010000000000000000"
    spl_object_hash($b = new stdClass())  // string(32) "00000000000000020000000000000000"
);

class A{}

class B{ private int $c = 1;}

class C
{
    public function x()
    {
        echo 'y';
    }
}

var_dump(
    // オブジェクトがメモリ上に保持されないため、同じ場所にオブジェクトが再割り当てされる
    // 結果、同じハッシュ値が返ってくる
    spl_object_hash(new stdClass()), // string(32) "00000000000000030000000000000000"
    spl_object_hash(new stdClass()), // string(32) "00000000000000030000000000000000"
    spl_object_hash(new A()), // string(32) "00000000000000030000000000000000"
    spl_object_hash(new B()), // string(32) "00000000000000030000000000000000"
    spl_object_hash(new C()), // string(32) "00000000000000030000000000000000"
);

 インスタンスがすぐにメモリ上から消えるコードの場合は上記例の様にハッシュ値が再利用されることになり衝突が頻発します。このため spl_object_hash はプロセス内や全世界の中で一意な値になることを期待すべきではありません。

 とはいえ使い物にならないわけではありません。例えば spl_object_hash を活用できるパターンとしてオブジェクトをキーにした連想配列でキーの元となったオブジェクトを保持するパターンがあります。具体的には次です。

<?php

$objCache = [];

$obj1 = new stdClass();
$obj2 = new stdClass();
// オブジェクトとそれに付随する情報をオブジェクトをキーにして保存する
$objCache[spl_object_hash($obj1)] = [
    'obj' => $obj1,
    'name' => 'hoge'
];
$objCache[spl_object_hash($obj2)] = [
    'obj' => $obj2,
    'name' => 'fuga'
];
var_dump($objCache);
//array(2) {
//  ["00000000000000010000000000000000"]=>
//  array(2) {
//    ["obj"]=>
//    object(stdClass)#1 (0) {
//    }
//    ["name"]=>
//    string(4) "hoge"
//  }
//  ["00000000000000020000000000000000"]=>
//  array(2) {
//    ["obj"]=>
//    object(stdClass)#2 (0) {
//    }
//    ["name"]=>
//    string(4) "fuga"
//  }
//}

 この例では、オブジェクトとそれに関連付けられたデータをspl_object_hashで得たハッシュ値をキーとする連想配列に保存しています。オブジェクトが破棄されることなく保持されるためspl_object_hashが返すハッシュ値はユニークであり続けます。

 もしクラスとプロパティの状態が同じであれば同一のオブジェクトとしてみなしても問題ないのであれば serialize を使うというのも手です。

PHP: serialize – Manual

 serialize は値を復元可能な文字列に変換する関数です。これを用いることでメモリ上、同じ位置に割り振られていたオブジェクトでも異なる値が返ってきます。これは例えば次の様になります。

<?php

var_dump(
// spl_object_hash では異なっていたが同じになる
    serialize($a = new stdClass()), // string(19) "O:8:"stdClass":0:{}"
    serialize($b = new stdClass())  // string(19) "O:8:"stdClass":0:{}"
);

class A{}

class B{ private $c = 1;}

class C
{
    public function x()
    {
        echo 'y';
    }
}

// spl_object_hash では全て同じになっていたが異なる部分もある
var_dump(
    serialize(new stdClass()), // string(19) "O:8:"stdClass":0:{}"
    serialize(new stdClass()), // string(19) "O:8:"stdClass":0:{}"
    serialize(new A()), // string(12) "O:1:"A":0:{}"
    serialize(new B()), // string(s27) "O:1:"B":1:{s:4:" B c";i:1;}"
    serialize(new C()) // string(12) "O:1:"C":0:{}"
);

 spl_object_hash はメモリの観点から serialize はインスタンスの元となったクラスと現状態の観点からオブジェクトに応じたユニークな文字列を返してくれる関数です。適宜必要なものを使えると作れるプログラムの幅が増えます。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG