LoginSignup
49
40

More than 1 year has passed since last update.

PHPの関数とメソッドは別物

Posted at

関数はメソッドの気取った言い換えではありません

今年に入ってから6回くらい別の人に個別に説明したので記事に書きます。

一目でわかる関数とメソッド

$array = [1, 2, 3];
// これは関数呼び出し
$length = count($array);

$object = new ArrayObject($array);
// これはメソッド呼び出し
$lenth = $object->count();
$datetime1 = date_create_immutable('2022-09-03'); // ← 関数呼び出しです
$datetime2 = DateTimeImmutable::createFromFormat('Y-m-d', '2022-09-03'); // ← (静的)メソッド呼び出しです
$datetime3 = $datetime2->createFromFormat('Y-m-d', '2022-09-03'); // ← メソッド呼び出しです
$datetime4 = new DateTimeImmutable('now'); // ← new式(インスタンス生成式)です

厳密に言うなら呼び出し構文だけで関数かメソッドかを判別はできないのですが、9割の呼び出しはこのパターンです。

もちろんこれはまったく網羅できていないのですが、この記事では便宜上このような名前で説明しておきます。

こんな曖昧な説明の分類に納得いかない人は言語仕様を読んでみてください。

PHPにおける関数とは何か

大雑把に言うと、hoge($arg)Foo\Bar\buz($arg)のように呼び出せるものです。

以下のようなファイルで宣言した関数はグローバル名前空間に属します。

hoge.php
<?php

function hoge(): void
{
    var_dump("hoge");
}

以下のようなファイルで宣言した関数はFoo\Bar名前空間に属します。

Bar.php
<?php
namespace Foo\Bar;

function buz(): void
{
    var_dump("buz");
}

function buzbuz(): void
{
    var_dump("buzbuz");
}

これらの関数は以下のように呼び出せます。

User.php
<?php
namespace Foo\Bar;
use function Foo\Bar\buzbuz;

class User
{
    public function test(): void
    {
        // 現在の名前空間の定義状況によって呼び出しが不定になるパターン
        hoge(); // Foo\Bar\hoge() 関数が定義されていればそれが、なければグローバルの hoge() が呼び出される
        buz();  // Foo\Bar\buz() 関数が定義されているので、それが呼び出される

        // 現在の名前空間の定義状況に関わらず決まった関数を呼び出しできるパターン
        buzbuz(); // ファイル冒頭でインポートされているので、確定で Foo\Bar\buzbuz() が呼び出される
        \hoge(); // グローバル空間に定義された関数だと明示しているので確定で hoge() が呼び出される
        \Foo\Bar\buz();  // ネームスペース含めてフル指定しているので確定で Foo\Bar\buz() が呼び出される
    }
}

PHPにおけるメソッドとは何か

クラスに属する手続き(関数のようなもの)です。

大雑把に言うと、$obj->foo($arg)Foo\Bar\User::create($arg)のように呼び出せるものです。

メソッドには以下の2パターンがあります。

  • インスタンスオブジェクトから->で呼び出せるメソッド
  • インスタンス化なしに::(スコープ定義演算子)からも呼び出せるメソッド (静的メソッド)

ややこしいのでこの記事では、静的ではないメソッドをインスタンスメソッドと呼ぶことにしましょう。
ただし正確な定義ではないので気をつけてください。

静的メソッドはstaticというキーワードで定義できます。

静的メソッドは静的にしか呼び出せないメソッド(Rubyでいうクラスメソッド)ではありません

メソッドは以下のように呼び出します。

$datetime = DateTimeImmutable::createFromFormat('Y-m-d', '2112-09-03');
//                             ~~~~~~~~~~~~~~~~
echo $datetime->format('Y-m-d H:i:s');
//              ~~~~~~

PHPにおいてはプロパティとメソッドは別の空間に属しています。

class X {
    public string $v = 'property';
    public function v(): string {
        return 'method';
    }
}

$x = new X();
echo "This is a ", $x->v, ".", PHP_EOL;
echo "This is a ", $x->v(), ".", PHP_EOL;

静的メソッドは::で呼び出し、インスタンスメソッドは->で呼び出す… という理解は96%くらいのユースケースにおいては正しいですが、正確な理解ではありません。静的メソッドを->で呼び出すこともできますし、インスタンスメソッドを::で呼ぶこともできる文脈もあります。あくまで::スコープ定義演算子です。

ここまでの記述を覆すようですが、PHP内部でもdebug_backtrace()Throwable::getTrace()では呼び出し方法に関係なく、呼び出されたメソッドが静的メソッドかという識別のための文字列として'::''->'が使い分けられています。動作確認: https://3v4l.org/QjBWV

さきほどPHPの静的メソッドはクラスメソッドではないと述べましたが、PHPでは同じクラスで public function foo()public static function foo()を同時定義することはできません。これはRubyのクラスメソッドと異なる重大なポイントです。

PHPにおけるクラスとコンストラクタとは何か

大雑把に言うと、new User()のようにインスタンスを生成したあとに暗黙的に呼び出されるものです。

class User extends AbstractUser
{
    public function __construct()
    {
        $this-> // ← この時点で既に $this にアクセス可能になっている
        parent::__construct();; // ← 親のコンストラクタを直接起動する必要がある
    }
}

PHPでは親クラスのコンストラクタは暗黙的に呼び出されることはないので、クラスを継承した場合は親クラスを正常に機能させるには明示的に呼び出さなければなりません。

PHPでは関数とクラスは別の空間に属しています。つまり、class Userとは別にfunction Userを定義できます。

<?php
namespace Foo\Bar;

function User()
{
    echo 'Called ', __FUNCTION__, PHP_EOL;
    return new User();
}

class User
{
    public function __construct()
    {
        echo 'Called ', __METHOD__, PHP_EOL;
    }

    public static function new()
    {
        echo 'Called ', __METHOD__, PHP_EOL;
        return new User();
    }
}

echo '[user1]', PHP_EOL;
$user1 = new User();
echo '[user2]', PHP_EOL;
$user2 = User::new();
echo '[user3]', PHP_EOL;
$user3 = User();

これはPythonのオブジェクト指向機能を知っていると混乱するかもしれませんね。

予約語 vs メソッド

多くのプログラミング言語と同じように、PHPの構文に使用するキーワード(予約語, reserved words)は関数名や定数名としては利用できませんが、PHP 7.0からはメソッドとクラス定数としてすべてのキーワードが許可されました

上記のページに掲載されているキーワードは関数名としては利用できませんが、メソッド名としては合法です。User::newのようなメソッドが存在できるのはこのためです。

ただしその他の予約語の一覧に含まれるキーワード(主に型名)は関数として定義可能です。
クラス名にはできません。

callable (呼び出し可能型)

PHPでは変数も関数のように呼び出し可能です。

$f1 = 'DateTimeImmutable::createFromFormat';
$result = $f1('Y-m-d', '2022-09-03');
var_dump($result);

$f2 = [DateTimeImmutable::class, 'createFromFormat'];
$result = $f2('Y-m-d', '2022-09-03');
var_dump($result);

関数のように呼び出せる値をcallableとして型宣言できます。

callableは単一のデータ型ではなく、要件を満たす値には以下のようなものがあります

  • string
    • 関数名文字列
    • 静的メソッド名文字列
  • array
    • [$object, 'method'] のような組の配列
    • ['クラス名', 'method'] のような組の配列
  • object

ただし現在となってはcallable型宣言をそのまま使うのではなく、以下の方法でClosureに変換した方がよいでしょう。

// 愚直に無名関数でくるむ方法、パラメータ宣言のコピペが必要なのでめんどい
$c1 = function (string $format, string $datetime, ?DateTimeZone $timezone = null) { 
    return DateTimeImmutable::createFromFormat($format, $datetime, $timezone);
};
$result = $c1('Y-m-d', '2022-09-03');
var_dump($result);

// メソッド名文字列からClosureに変換する
$c2 = Closure::fromCallable('DateTimeImmutable::createFromFormat');
$result = $c2('Y-m-d', '2022-09-03');
var_dump($result);

// PHP 8.1から追加された第一級callableを生成する記法
$c3 = DateTimeImmutable::createFromFormat(...);
$result = $c3('Y-m-d', '2022-09-03');
var_dump($result);

今回の記事の本題ではないので触れませんが、PHPStanではClosurecallableにも詳細に型付けできます

なぜあなたは混乱するのか

ほかの言語についての知識が前提にあり、PHPとの違いを把握できていない可能性があります。
特に、メソッドだけがあって関数が存在しない言語に触れた経験があると、すべてをメソッドと呼んでしまいがちです。

PHPの「無名関数」(Anonymous functions, Closure)に相当するものはC#では「匿名メソッド」(Anonymous methods)と呼ばれる、などの事例が混乱に拍車を掛けます。

Rubyの場合

Rubyのメソッド呼び出しは基本的には obj.method(arg) のような形式です。objの部分は「レシーバ」と呼ばれますが、さまざまな場面で obj. が省略できます。また、メソッド呼び出しmethod(arg)はメソッドチェーンが不要な場合はmethod argと書かれることも多いです。

require 'pp'
#~~~~~~

class Foo
  attr_reader :name
# ~~~~~~~~~~~

  def hoge
    @name = 'name'.upcase
#                 ~~~~~~~
    p bar
#   ~ ~~~
  end

  private def bar
# ~~~~~~~
    puts self.name
#   ~~~~     ~~~~~
    'success'
  end
   :bar
end

Rubyでは requireprivate, exit, raise (PHPのthrowに相当)のような、ほかの言語では言語構文の一部であるようなものもメソッド呼び出しです。また、pputs, printf, rand のような標準関数のようなもの、PHPではマジック定数として提供されている __method__, __dir__ のようなものもすべてカーネルモジュールで定義されています

これらの関数的に使われるメソッドはRuby用語ではモジュール関数といい、ObjectにはKernelモジュールが暗黙的にincludeされているので、これらのメソッドはあたかも標準関数や言語の構文かのように使うことができるのです。

よってRubyでは「関数⊂メソッド」です。PHPでいうプロパティのようなself.nameですらも、attr_accessorattr_readerのようなアクセサメソッドが間に入ることで提供されています。

PHPにおけるメソッドはクラスに属するものですが、Rubyは特異メソッドといって、インスタンスオブジェクトひとつひとつに固有のメソッドを定義できます。RubyはクラスそのものもClassクラスのインスタンスオブジェクトであり、クラスメソッドはクラスの特異メソッドに過ぎません。

これらの機能がどうやって実現されているか、PHPの静的メソッドとRubyのクラスメソッドの決定的な差異など、Rubyのオブジェクトシステムには語りたいことがたくさんあるのですが、この記事の趣旨からは逸れるのでこのあたりでやめておきます。気になる人はRubyソースコード完全解説第1章 Ruby言語ミニマム第4章 クラスとモジュールを読んでください。
PHP固有の事情についてもややこしい話があるので、また別の記事の機会に譲りましょう。

Javaの場合

PHPやC++には関数とメソッドの両方がありますが、Javaに関数はありません

PHPやRubyで標準ライブラリ的に用意されているprintf()はJavaではSystem.out (PrintStream)のメソッドとして提供されています。

Javaで関数呼び出しのように見えるものは、this.を省略したメソッド呼び出しです。

class Hello {
    public static void main(String[] args) {
        sayhello();
    }
    
    public static void sayhello() {
        System.out.printf("%s", "Hello");
    }
}

PHPではこのような暗黙的な$this->呼び出しはありません。

<?php

function sayhello(): void
{
    echo __FUNCTION__, ' function', PHP_EOL;
}

class Hello
{
    public __construct()
    {
        sayhello();
        $this->sayhello();
    }

    public static function sayhello: void
    {
        echo __METHOD__, ' method', PHP_EOL;
    }
}

$hello = new Hello();

メソッドと同名の関数があろうがなかろうが、sayhello()は関数呼び出し、$this->sayhello()はメソッド呼び出しであり、混同されることはありません。

Pythonの場合

Pythonはここまでに挙げた二つの言語とは異なり、関数とメソッドをよく区別する言語です。

たとえばPythonのsortedlist.sortの使い分けは初心者には意味不明なほどPythonの思想を表しています。

# 順不同の fruits リストを作る
>>> fruits = ['orange', 'apple', 'strawberry', 'banana']
# sorted 関数はソートされた新しいリストを返す
>>> sorted_fruites = sorted(fruits)
>>> sorted_fruites
['apple', 'banana', 'orange', 'strawberry']

# fruits は順不同のまま
>>> fruits
['orange', 'apple', 'strawberry', 'banana']
# list.sort メソッドを呼ぶと、オブジェクト自体がソート済みに変化する
>>> fruits.sort()
>>> fruits
['apple', 'banana', 'orange', 'strawberry']

Pythonでは文字列やリストなどのような「長さ」という性質を持ったオブジェクト(シーケンス)の長さを取得する際はlen(v)、文字列に変換できるオブジェクトを文字列化するときはstr(v)のように、汎用の操作は組み込み関数の呼び出しを通じてアクセスするのが一般的な習慣です。その一方でクラスに特殊メソッドを定義することで、オブジェクトのさまざまな振る舞いを柔軟にカスタマイズできる設計になっています。

PHPにもPythonの特殊メソッドに相当するマジックメソッドが定義できますが、__construct()を除けば一般に活用されているものはあまり多くないでしょう。PHPではPythonのlenに相当するcount()関数で、長さを公開しているオブジェクトはCountableインターフェイスとして分離されています。
(string)キャストなどで文字列化するときの振る舞いを制御する__toString()マジックメソッドもありますが、これもPHP 8.0以降はCountableと同様にStringableインターフェイスに分離し、Stringable::__toString()として明示的に実装する方向に移りつつあります。
PHPの文字列はcount()ではなく、バイト数を取得するstrlen()と文字コードごとの文脈での文字列長(コードポイント数)を返すmb_strlen()を使い分ける必要がありますが、そもそもPHPはPythonとは異なりバイナリ列とUnicode文字列を型レベルで区別できないため、現状においては妥当な区別と言えるでしょう。もっとも現状においてはSymfony Stringのようなユーザーランド実装があるほか、PHPコア開発者のDerick Rethans氏はThe PHP Foundationのコア開発者の所信表明にてUStringを導入したいと意欲を示しているので、PHP 10くらいの頃には状況が変わってるのではないのでしょうか。

これらのポイントを除いてもPythonとPHPのクラスはそもそもいろいろ違う(メソッドのパラメータに明示的にselfを含める必要がなく最初から$thisが特殊な変数として束縛されている、@classmethod@staticmethodの使い分けがない、関数とクラスが別の名前空間なのでFoo()関数とFooクラスが別に存在できるなど)ので、Python経験者が無造作にPHPを書こうとするとひっかかる点は多いのではないかと思います。

まとめ

カタカナ語が多くて覚えるの大変だとは思うのですが、技術者としては言葉の意味を大切にして仕事していきたいですね。

49
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
40