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

structの関数プロパティとメソッドの挙動

package main

import "fmt"

type OpFunc func(int, int) int

type Operation struct {
	// associateされていないのでOperationのプロパティ・メソッドへのアクセスは出来ない。
	Do1 OpFunc
	// 以下コメントを外すと "type Operation has both field and method named Do2"
	//Do2 OpFunc
}

func (op Operation) Do2(a, b int) int {
	return op.Do1(a, b)
}

func main() {
	op := &Operation{
		Do1: func(a, b int) int {
			return a + b
		},
	}
	fmt.Println(op.Do1(1, 2), op.Do2(3, 4))
}

http://play.golang.org/p/4ytA1cOJ1d

golangのType Assertionメモ

  • Type AssertionはC++のdynamic_cast的な機能。
  • interfaceを別の型にキャストする時に使用。
  • 2通りの受け方がある。
package main

import (
	"fmt"
	"errors"
)

type MyError struct {
	i int
}

func (e *MyError) Error() string {
	return fmt.Sprintf("i = %d", e.i)
}

func check1(e error) {
	myErr, ok := e.(*MyError)
	if ok {
		fmt.Println(myErr)
	} else {
		fmt.Println("It's not *MyError")
	}
}

func check2(e error) {
	myErr := e.(*MyError)
	if myErr != nil {
		fmt.Println(myErr)
	} else {
		fmt.Println("It's not *MyError")
	}
}

func main() {
	e100 := &MyError{100}
	e200 := errors.New("200")
	check1(e100)
	check1(e200)
	check2(e100)
	check2(e200) // panic
}

http://play.golang.org/p/MY2G3J_QPU

package main

import (
	"fmt"
	"strconv"
)

func Println(x interface{}) {
	if i, ok := x.(int); ok {
		fmt.Println(strconv.Itoa(i))
		return
	}
	if i, ok := x.(int64); ok {
		fmt.Println(strconv.FormatInt(i, 10))
		return
	}
	if _, ok := x.(string); ok {
		fmt.Println("ニヤ(・∀・)ニヤ")
		return
	}
}

func main() {
	Println(int(100))
	Println(int64(200))
	Println("300")
}

http://play.golang.org/p/Qt7EPb-4kF