PHPの小ネタ: タイプヒンティングの罠

タイプヒンティングとは

PHP5からタイプヒンティングという似非タイプチェック機能が使えるようになっています。

<?php
interface Runnable
{
    public function run();
}

class Foo implements Runnable
{
    public function run()
    {
        echo 'Foo', "\n";
    }
}

class Bar
{
    public function run()
    {
        echo 'Bar', "\n";
    }
}

function run(Runnable $runner)
{
    $runner->run();
}

run(new Foo);
run(new Bar);
Foo
PHP Catchable fatal error:  Argument 1 passed to run() must implement interface Runnable, instance of Bar given

なるほど中々悪くなさそうな機能です。 が、このタイプヒンティングという機能には、色々と罠が潜んでいます。

スカラー型には使えない

ちゃんとマニュアルを読みましょうという話ですが、

タイプヒントは int や string といったスカラー型には使えません。 また、リソース や トレイト も使えません。

http://php.net/manual/ja/language.oop5.typehinting.php

と書かれています。

<?php
function add(int $a, int $b) // NG
{
    return $a + $b;
}

ついついやりたくなるんですけどね…。

nullは渡せない、が渡せないこともない

何言っているのかというと、こういうことです。

<?php
function echoTimestamp(DateTime $dt)
{
    echo ($dt ? $dt->getTimestamp() : 0), "\n";
}

echoTimestamp(null);
PHP Catchable fatal error:  Argument 1 passed to echoTimestamp() must be an instance of DateTime, null given,

なるほど、nullは渡せないのか! と思いきや。

<?php
function echoTimestamp(DateTime $dt = null)
{
    echo ($dt ? $dt->getTimestamp() : 0), "\n";
}

echoTimestamp(null);
0

デフォルト引数にnullを与えている場合は、null値を引数に渡せます。なんじゃそりゃ。

しかし、デフォルトのパラメータの値として NULL を使用した場合は、後から任意の値を引数に指定できるようになります。

http://php.net/manual/ja/language.oop5.typehinting.php

デフォルト引数が無いときにnull値を与えられないというのは、メソッド内部でnullかどうかで分岐してないということが保証されるので、それはそれで良いのかな*1と良心的に解釈できますが、デフォルト引数nullの仕様は完全に蛇足な気がします。

<?php
function addSeconds(DateTime $dt = null, $seconds = 0)
{
    if (is_null($dt)) {
        $dt = new DateTime('now');
    }
    return $dt->add(new DateInterval("PT{$seconds}S"));
}

$dt = new DateTime('now');
echo $dt->format('H:i:s'), "\n";
echo addSeconds($dt, 300)->format('H:i:s'), "\n";
echo addSeconds(null, 300)->format('H:i:s'), "\n";

全部にデフォルト引数与えるの、なんか違わない?引数なしで呼べるけど意図した使い方じゃないよね、という…。

所感

タイプヒンティングは、nullが来ないことを保証されているのだとポジティブに考えて、上手に使うのが良いのかと思います。

hacklangではもっと厳密に型を宣言できるようなので、一度試してみたい所です。

*1:nullでの分岐をしたくなるシチュエーションはそれなりにありますが、そういうことをするとメソッドブラックボックス度は上がるのでそれが矯正されるなら良くかなという