"マジックメソッドはメソッドだから呼ばれれば動く"件について
元ネタは、 https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022?slide=84 を見て「コンストラクタが2回動くの!?」って質問があったので。
端的には「PHPにおいてコンストラクタはマジックメソッドで、マジックメソッドはメソッドだから、callすれば動く」っていうのが身も蓋もない回答になります。
かみ砕いて。
まずPHPにおける「マジックメソッド」は、以下の通り。
https://www.php.net/manual/ja/language.oop5.magic.php
マジックメソッドは、 ある動作がオブジェクトに対して行われた場合に、 PHP のデフォルトの動作を上書きする特別なメソッドです。
"ある動作がオブジェクトに対して行われた場合"をトリガーに"PHP のデフォルトの動作を上書きする特別なメソッド"、なので、基本「メソッド」です。
以下の関数名は、マジックメソッドと見なされます: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone(), __debugInfo()
とあるように、マジックメソッドです。
なので、PHPの挙動としては
・インスタンスを作る
→ "インスタンスを作る"動作がオブジェクトに対して行われた場合、 PHPのデフォルトの動作を上書きする __construct() メソッドがあるなら、そいつを実行する
ってな感じになります。
なのでまぁいわゆる「コンストラクト時の動作を __construct() メソッドに書いておけば」インスタンスが生成される時に「明示的には呼んでないけど、自動でcallされる」んですね。
んで。
とはいえまぁ__construct()くんも「メソッド」なので。
「明示的にcallされた」ら、それはまぁ「呼ばれたから動く」わけなんですね。
かくしてまぁ。元ネタのDateTimeImmutableで「コンストラクタを明示的に叩くとコンストラクタが呼ばれる(ので、値が書き換えられる」ってのは、まぁ「ですよね~」くらいの挙動になります。
「はて他言語はどうなってるんだろ?」と思い、なんとかするっと目に書けるC++を(幾分思い出しつつ)書いてみたのですが。
#include <iostream> class Hoge { public: // コンストラクタ Hoge() { std::cout << "construct" << std::endl; } }; int main() { Hoge *h = new Hoge(); h->Hoge(); return 0; }
うんそもそもコンストラクタはコンストラクタであって「クラス関数ではない」んだよなぁ。
なのでまぁ
t.cpp: 関数 ‘int main()’ 内:
t.cpp:13:8: エラー: 無効な ‘Hoge::Hoge’ の使用です
h->Hoge();
^
って言われるですだよ。うん。
……それ以外の言語はど~なんだろうねぇ?*1
話を戻して。
なのでまぁ、PHPのコンストラクタは所詮「メソッド」なので、「呼べば動く」ですます。
お仕事で考える場合、この辺は「明示的に呼ぶな」で終わっていいような気がするんだよなぁ……なんていうか「そこまで配慮せにゃならんのを現場で使いたいかね?」とか、正直。
ただまぁ「思考実験として」であれば、例えばこんな感じか。
<?php declare(strict_types=1); error_reporting(-1); class Hoge { public function __construct() { // コンストラクタ二重callのロック機構 static $double_call_lock = []; $object_id = spl_object_id($this); if (null !== ($double_call_lock[$object_id] ?? null)) { throw new \Exception('コンストラクタ明示的に呼ぶとか、おまえ、正気か?'); } $double_call_lock[$object_id] = true; } } // $obj = new Hoge(); $obj2 = new Hoge(); $obj3 = new Hoge(); //$obj->__construct();
ただ、これ(spl_object_id)も「このオブジェクトidはオブジェクトが生きている間ユニーク」なので。
// $obj = new Hoge(); $obj = new Hoge(); $obj = new Hoge();
ってやると
[gallu@ik1-111-11111 ~]$ php t.php
int(1)
int(2)
int(1)Fatal error: Uncaught Exception: コンストラクタ明示的に呼ぶとか、おまえ、正気か? in /home/gallu/t.php:14
Stack trace:
#0 /home/gallu/t.php(23): Hoge->__construct()
#1 {main}
thrown in /home/gallu/t.php on line 14
ってな感じになる事があるんで、あんまり十全に大丈夫でもなし。
いやまぁがっちょりとデストラクタまで書いて
<?php declare(strict_types=1); error_reporting(-1); class Hoge { public function __construct() { // コンストラクタ二重callのロック機構 $object_id = spl_object_id($this); var_dump($object_id); if (null !== ($this::$double_call_lock[$object_id] ?? null)) { throw new \Exception('コンストラクタ明示的に呼ぶとか、おまえ、正気か?'); } $this::$double_call_lock[$object_id] = true; } public function __destruct() { // 「コンストラクタ二重callのロック機構」の解除 $object_id = spl_object_id($this); echo "unset {$object_id}\n"; unset($this::$double_call_lock[$object_id]); } private static $double_call_lock = []; } // $obj = new Hoge(); $obj = new Hoge(); $obj = new Hoge(); //$obj->__construct();
ってやれば多分
[gallu@ik1-111-11111 ~]$ php t.php
int(1)
int(2)
unset 1
int(1)
unset 2
unset 1
ってな具合にはなるんだろうけど、「そこまで書く?」って話になりそうだしねぇ。
なのでまぁ「コンストラクタで値を設定、そのインスタンスの値は不変」っていうのは、PHPの場合ある程度「善意と良識」に守られている所があるんだろうなぁ、と。
ただまぁ「その辺を横紙に破りまくるような御方」は、ちょっと……って思うので。
その辺はまぁ「どーゆースタンスを持つか」次第なんじゃないかなぁ? とか思うんだけど、ど~なんだろうねぇ???
*1:コメントとかでいただければ追記いたしまする