がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

"マジックメソッドはメソッドだから呼ばれれば動く"件について

元ネタは、 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 のデフォルトの動作を上書きする特別なメソッド"、なので、基本「メソッド」です。

んで、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:コメントとかでいただければ追記いたしまする