PHPでオーバーロードしたい

オーバーロードは空気のように当たり前の存在だったので、当然のようにPHPでやってしまってエラーが出た。エラーメッセージの意味を理解するのに数秒かかったくらいだ。そのとき、

オーバーロードは実行時のオブジェクトの型ではなく、変数の型が分からないと実現できないので、PHPでは実現が難しいのでは...

と思ったので調べてみた。メモメモ。
静的型を持つ多くの言語では、シグネチャを変えることで同じ名前メソッドを複数個定義できる。この仕組みはオーバーロードと呼ばれる。オーバーロードされた複数のメソッドのうち、実際どのメソッドが呼ばれるかはコンパイル時に決まってしまうので、オーバーロードを使わなくても別の名前のメソッドしてしまえば全く問題ない。つまり、オーバーロード無しではプログラミングできないというものではない。しかし、メソッドの名前を同じにすることで、同じ意味のメソッドであるということを明示でき、その結果プログラマの負担を減らせるという点でとても重要なんじゃないのかな。
次のようなC#のPersonクラスのGreetメソッド(C#の動作は未確認)に相当することをPHPで実験してみた。

// 人を表すシンプルなクラス
class Person
{
  private string _name;

  private string _sex;

  private DateTime _birthday;

  public Person(string name, bool sex, DateTime birthday)
  {
    _name = name;
    _sex = sex;
    _birthday = birthday;
  }

  public string Name
  {
    get { return _name; }
  }

  public bool Sex
  {
    get { return _sex; }
  }

  public DateTime Birthday
  {
    get { return _birthday; }
  }

  // パターン1:人を指定しないと自己紹介する
  public void Greet()
  {
    Console.WriteLine("Hi, I'm {0}.", _name);
  }

  // パターン2:引数で指定した人にあいさつする
  public void Greet(Person person)
  {
    Console.WriteLine("Hi, {0}.", person.Name);
  }

  // パターン3:パターン2の動作に加えて追加のセリフ付き
  public void Greet(Person person, string message)
  {
    Console.WriteLine("Hi, {0}. {1}", person.Name, message);
  }

  public override string ToString()
  {
    return
      string.Format("name={0} sex={1} birthday={2}",
        _name, _sex, _birthday);
  }
}

参考にさせて頂いたサイト:PHPのオーバーロード - Shin x blog
毎度のことだけど、間違ったこと書いてる可能性はあるので要注意。コメント欄にてツッコミください。

空の仮引数リストを持つメソッドによる方法

<?php
// 人を表すシンプルなクラス
class Person
{
  // stringで名前
  private $_name;

  // boolで性別
  private $_sex;

  // DateTimeで誕生日
  private $_birthday;

  public function __construct($name, $sex, $birthday)
  {
    $this->_name = $name;
    $this->_sex = $sex;
    $this->_birthday = $birthday;
  }

  public function GetName()
  {
    return $this->_name;
  }

  public function GetSex()
  {
    return $this->_sex;
  }

  public function GetBirthday()
  {
    return $this->_birthday;
  }

  // オーバーロードされるGreetメソッド
  public function Greet()
  {
    // 引数の並びを取得
    $args = func_get_args();
    $nArgs = count($args);

    // パターン1
    if ($nArgs == 0)
    {
      echo "Hi, I'm {$this->_name}.";
    }
    // パターン2
    else if ($nArgs == 1)
    {
      echo "Hi, {$args[0]->GetName()}.";
    }
    // パターン3
    else if ($nArgs == 2)
    {
      echo "Hi, {$args[0]->GetName()}. {$args[1]}";
    }
    // 未定義
    else
    {
      // unexpected method call
    }
  }

  public function ToString()
  {
    return
      "name=". $this->_name .
      " sex=" . ($this->_sex ? "F" : "M") .
      " birthday=" . $this->_birthday->format("Y-m-d");
  }
}
?>

どんな引数の並びで呼び出しても、引数なしの形のメソッドを呼びさせるなんて!しかも引数の並びを取得できるメソッドがあるなんて!カルチャーショック。

__callによる方法

<?php
// 人を表すシンプルなクラス
class Person
{
  // stringで名前
  private $_name;

  // boolで性別
  private $_sex;

  // DateTimeで誕生日
  private $_birthday;

  public function __construct($name, $sex, $birthday)
  {
    $this->_name = $name;
    $this->_sex = $sex;
    $this->_birthday = $birthday;
  }

  public function GetName()
  {
    return $this->_name;
  }

  public function GetSex()
  {
    return $this->_sex;
  }

  public function GetBirthday()
  {
    return $this->_birthday;
  }

  public function __call($method, $args)
  {
    $nArgs = count($args);
    if ($method == "Greet" &&
      0 <= $nArgs &&
      $nArgs <= 2)
    {
      // パターン1
      if ($nArgs == 0)
      {
        echo "Hi, I'm {$this->_name}.";
      }
      // パターン2
      else if ($nArgs == 1)
      {
        echo "Hi, {$args[0]->GetName()}.";
      }
      // パターン3
      else
      {
        echo "Hi, {$args[0]->GetName()}. {$args[1]}";
      }
    }
    // 
    else
    {
      // unexpected method call
      var_dump($method);
      var_dump($args);
    }
  }

  public function ToString()
  {
    return
      "name=". $this->_name .
      " sex=" . ($this->_sex ? "F" : "M") .
      " birthday=" . $this->_birthday->format("Y-m-d");
  }
}
?>

__callというのは、メソッドに限らず、定義されていないメンバにアクセスした場合に呼ばれるメソッドらしい。引数で

  1. アクセスしようとしたメンバの名前
  2. 引数(引数を伴ったメソッド呼び出しを試みた場合のみ?)

が調べられるので、あとは条件分岐するだけ。
重要なのは、オーバーロードしようとしているメソッドを定義してはならないこと。Ω<ナ、ナンダッテー

まとめ

同じ名前のメソッドは2個以上定義できないという事実は変えようがないのかもしれないけど(詳しくは分からない)、方法は違うものの

  • 引数の並び
  • メソッドの名前

を取得する方法が提供されているみたいだ。それらをチェックして条件分岐で挙動を変えたらよいのかな。
便利かというと、ちょっと微妙。全部同じメソッドの中に書いちゃうと長くなりそうなので、privateなメソッドに処理を委譲するのがよいのだろうけど、そうなると別の名前を付けて定義するのとあんまり変わらんな。外部に公開するメソッドを1つにできるという点では価値がありそうかも?それぞれ欠点を考えてみた

  • 引数なしのメソッドによる方法
    • 外部には引数無しのメソッドとして見えてしまう
  • __callによる方法
    • 引数の個数うんぬん以前に、外部からは意図しているメソッドが存在するのかどうかすらも分からない
    • 存在しないメンバに対するアクセスを全て捕捉してしまうので、ちゃんとエラー処理しないと、簡単にエラーを見過ごしてしまいそうで怖い

自分としては、前者の方法の方が良いようなきがする。
多くのスクリプト言語で、__callに相当するメソッドが提供されているらしい。


っていうか、C#のプロパティの仕組みも便利だよなぁー。