質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.12%

class内のconstは後から定義しても良いのはなぜか?

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 5
  • VIEW 1,611

raccy

score 18385

PHPのconstについて、

<?php
const Y = "hoge";
const X = Y;
var_dump(X);


これはstring(4) "hoge"が表示されます。では、

<?php
const X = Y;
const Y = "hoge";
var_dump(X);


とすると、Yを定義せずに使っているという警告と共にstring(1) "Y"が表示されます。ではでは、

<?php
class A {
    const X = A::Y;
    const Y = "hoge";
}
var_dump(A::X);


とすると、string(4) "hoge"が表示され、定義前のA::Yを見に行けているようです。

どうしてclass外では後方のconstを参照することはできないのに、class内だとできるのでしょうか?そのとき、どのような順番で何を処理しているのでしょうか?

他の言語からの考察

C++はそもそも前方に宣言がないと使えないので、宣言があることが前提になります。char *のようなポインタだと逆順でも同じ物になりますが、std::stringのようなものでは逆順だと結果が変わります。

#include <iostream>
#include <string>
class A
{
public:
    static const char *const XC;
    static const char *const YC;
    static const std::string XS;
    static const std::string YS;
};

const char *const A::XC = YC;
const char *const A::YC = "hoge";
const std::string A::XS = YS;
const std::string A::YS = "hoge";

int main()
{
    std::cout << A::XC << std::endl; // hoge
    std::cout << A::XS << std::endl; // (空)
    return 0;
}

Javaはコンパイル時にエラーになります。

class A {
    public static final String X = Y; // エラー: 前方参照が不正です
    public static final String Y = "hoge";
    public static void main(String[] args) {
        System.out.println(X);
    }
}

Rubyは未定義であるとして同じくエラーになります。

class A
  X = A::Y # uninitialized constant A::Y (NameError)
  Y = "hoge"
end
p A::X

Haskellはオブジェクト指向におけるクラスに相当する物はないですが、通常の定数(そもそも全てが定数ですが)についても、遅延評価なので、順番とかはなんも関係無く動作できます。

x = y
y = "hoge"
main = print x -- "hoge"

C++がそういう動作になる理由はちょっと複雑になるので除きますが、JavaやRubyはclass内の定数であっても、定義されていない定数は使えません。定数の定義処理を順番にしていくと考えれば妥当な仕様だと思います。しかし、PHPだと、順番に定義していくと考えることはできません。となると、A::Y部分をHaskellのように遅延評価しているとか、そんな感じなのでしょうか?

参考にした質問

PHP - 定数の定義より前にその定数を使う関数の定義を書いても大丈夫なのか(73837)|teratail
上の質問については、関数内の式の評価が呼び出し時だから問題ないという物です。では、constの右辺の評価も呼び出し時だから…と考えると、Xの右辺が評価されるときにYの右辺が評価済みというのもおかしな話ですし、class外だとうまくいかない理由も付けられなくなります。


定数式のバグ - Qiitaを読んで、ふと気になりました。なお、この現象は記事に書いているバグと関係はありますが、バグそのもののことではありません。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

+4

umyuさんがコンパイルするんだよと言う情報とJunSuzukiJapanさんの推測、及び、PHP のクラス定数のちょっと奇妙な話 - ngの日記を参考にオペコードなどを調べてみました。

const_test.php

<?php
function f0() {
    echo A::X;
}
function g0() {
    echo Y;
}
class A {
  const X = 'ax';
}
const Y = 'y';
function f1() {
    echo A::X;
}
function g1() {
    echo Y;
}

と言うコードをオペコードを出してみたところ下記のようになっていました。

L1-18 {main}() const_test.php - 0x10d665540 + 7 ops
 L2    #0     NOP
 L5    #1     NOP
 L8    #2     NOP
 L11   #3     DECLARE_CONST           "Y"                  "y"
 L12   #4     NOP
 L15   #5     NOP
 L18   #6     RETURN                  1

function name: f0
L2-4 f0() const_test.php - 0x110477060 + 3 ops
 L3    #0     FETCH_CLASS_CONSTANT    "A"                  "X"                  ~0
 L3    #1     ECHO                    ~0
 L4    #2     RETURN                  null

function name: g0
L5-7 g0() const_test.php - 0x103c77120 + 3 ops
 L6    #0     FETCH_CONSTANT                               "Y"                  ~0
 L6    #1     ECHO                    ~0
 L7    #2     RETURN                  null

function name: f1
L12-14 f1() const_test.php - 0x10dc720c0 + 2 ops
 L13   #0     ECHO                    "ax"
 L14   #1     RETURN                  null

function name: g1
L15-17 g1()const_test.php - 0x103a772a0 + 3 ops
 L16   #0     FETCH_CONSTANT                               "Y"                  ~0
 L16   #1     ECHO                    ~0
 L17   #2     RETURN                  null

注目すべきはf0f1の違いとf1g1の違いです。f1のみ定数をFETCH_CLASS_CONSTANT等で取りに行かずに固定された(計算済みの)文字列になっています。つまり、

  • Compilationによってオペコードが終わった時点でクラス定数については確定済みにされる。対して、トップレベルの定数はDECLARE_CONSTによって動的に定義される。
  • 読みに行くクラス定数が既に現れている場合は、FETCH_CLASS_CONSTANTを使わずにその定数の値に置き換わる。

といえるかと思います。このことからクラス定数は静的、トップレベル定数は動的と言えるのかも知れません。しかし、今問題になっているのは後から定義の定数を使っている場合です。その場合についてもオペコードを見てみました。

<?php
function f0() {
    echo A::X;
}
function g0() {
    echo A::Y;
}
class A {
  const X = A::Y;
  const Y = 'ay';
}
function f1() {
    echo A::X;
}
function g1() {
    echo A::Y;
}
L1-18 {main}() const_test2.php - 0x10c8780c0 + 6 ops
 L2    #0     NOP
 L5    #1     NOP
 L8    #2     NOP
 L12   #3     NOP
 L15   #4     NOP
 L18   #5     RETURN                  1

function name: f0
L2-4 f0() const_test2.php - 0x103477060 + 3 ops
 L3    #0     FETCH_CLASS_CONSTANT    "A"                  "X"                  ~0
 L3    #1     ECHO                    ~0
 L4    #2     RETURN                  null

function name: g0
L5-7 g0() const_test2.php - 0x102877120 + 3 ops
 L6    #0     FETCH_CLASS_CONSTANT    "A"                  "Y"                  ~0
 L6    #1     ECHO                    ~0
 L7    #2     RETURN                  null

function name: f1
L12-14 f1() const_test2.php - 0x107277240 + 3 ops
 L13   #0     FETCH_CLASS_CONSTANT    "A"                  "X"                  ~0
 L13   #1     ECHO                    ~0
 L14   #2     RETURN                  null

function name: g1
L15-17 g1() const_test2.php - 0x10be72100 + 2 ops
 L16   #0     ECHO                    "ay"
 L17   #1     RETURN                  null

A::Yは確定した値に置き換わっていますが、A::Xは未定義前と同じくFETCH_CLASS_CONSTANTを使用しています。あとはA::XFETCH_CLASS_CONSTANTを使うように定義されていれば、この動作に説明が付くでしょう。しかし、ここからはオペコードからはわかりません。オペコードにしているコンパイルの処理を見なければならないようです。

ということで、クラスに定数を追加しているところをソースコードから確認しました。

  • zend_declare_class_constant_ex()<zen_API.c >にて、zend_class_entryのconstants_table(HashTableという型のハッシュテーブル)にzend_hash_add_ptr()で追加することでクラスの定義が行われます。
  • zend_compile_class_const_decl()<zend_complie.c>にて、astから名前のastと値のastを取得し、値のastをzend_const_expr_to_zval()に投げた結果のzvalをzend_declare_class_constant_ex()に投げています。
  • zend_const_expr_to_zval()<zend_compile.c>にて、zend_compile_const_expr()の処理があり、クラス定数ならさらにzend_compile_const_expr_class_const()が呼ばれまてastがzvalを取れる形に変換されます。
  • zend_compile_const_expr_class_const()にて、クラス定数がまた未定義の時は、astをクラス名::定数名を見に行くzvalに変換します。

PHPにそこまで詳しくないので間違っているかも知れませんが、上のような動きのようです。ということで、ソースコードを見る限り、JunSuzukiJapanさんの推測が正しいと思われます。ベストアンサーはJunSuzukiJapanさんに上げたいと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

checkベストアンサー

+2

PHPの内部はよく知りませんが、他の言語などから推察してみます。

<?php
const Y = "hoge";
const X = Y;
var_dump(X);

例えば、上記のコードの場合、PHPインタプリタは

・定数Yを定義し、その値を"hoge"とする。
・定数Xを定義し、その値を「定数Yの値("hoge")」にする。
・Xの値を表示

という動作をします。

では、問題の

<?php
class A {
    const X = A::Y;
    const Y = "hoge";
}
var_dump(A::X);

を考えてみます。

細かいところを省くと、上記のコードは

・クラスAを定義する。
・A::Xの値を表示

という動作をしています。

「クラスAを定義する」というところを、もう少しくわしく見ていきましょう。

クラスAを定義するとは

1。クラスAを作成(する準備をする)
2。クラスAに定数Xを定義し、その値をA::Yとする。
3。クラスAに定数Yを定義し、その値を"hoge"とする。

てなところでしょう。

問題は2の「クラスAに定数Xを定義し、その値をA::Yとする」ですが、
言語を実装する場合、2種類の方法が考えられます。

・クラス定義時にA::Yを計算してしまう。
・クラス定義時には「A::Yを計算した値である」と定義だけしておき、
はじめて参照された時に計算し、その後の変更はさせない。

PHPは後者の方法で実装されているのだと思われます。

なぜPHPが後者の方法で実装されているかは、実装した人たちに聞くなり、PHPの開発者メーリングリスト(あるかどうかはしらない)などを調べるなりしないとわからないです。

(個人が趣味で作った言語なら「そういう風に実装してみたかったから」などの理由だったりしますが、PHPだとどうでしょうね…)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/02 20:36

    A::XはA::Yの値ではなく参照を持っていると言うことでしょうか?参照扱いだったら、後から定義されても問題ないとは言えそうです(普通の変数であれば、`$x = &$y;$y = "hoge";var_dump($x);`って書けますし)。

    なにか、言語仕様としてそういう風になっているというドキュメントがあれば一番良いのですが。

    キャンセル

  • 2017/05/02 21:19

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

    ↑上記のページに

    > 注意:
    > 定数における式のサポートは PHP 5.6.0 で追加されました。

    ってあるのが、関係ありそうですね。

    キャンセル

+1

PHPの内部の処理はC言語(C++ではありません)で動いています。

PHPをC言語に変換した場合、素のconstは当然上から順番に処理されるので未定義のものを使用するとエラーが発生します。

しかしclassの場合はC言語にはclassが存在しませんのでPHPのclassは解析処理を通して構造体(struct)に変換する必要があります。
その際にクラス内の変数やconstは適切に参照され問題なく動作しているのでしょう。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/04/28 18:19

    PHPはC言語に変換して動いているということでしょうか?私自身、PHPにはあまり詳しくなったとはいえ、それなりに知っていたと思ったのですが、C言語に変換しているというのは知らなかったので驚きの新事実です。

    キャンセル

  • 2017/05/01 18:36

    PHPのclassがCのstructに変換されているとのことですが、具体的にconstはどのように変換されているのでしょうか?また、たとえstructに変換しても、C言語は事前に宣言されていない変数は使用できないと思うのですが、どうしてclassだと適切に参照されるのでしょうか?

    キャンセル

  • 2017/05/02 15:30 編集

    わかりやすくC言語に変換と言ってしまいましたが、PHPの実行ファイルがやっていることはPHPの構文が含まれる文字列を構文解析器が読み取っているだけです。

    構文解析器が全てメモリ上で処理している可能性もありますが、もし単純にC言語に書き換えていただけだとしても、構造体は事前に型枠の定義の宣言が必要になります。
    構造体の宣言前に構造体の中身を使用することはできませんので、classを構造体という枠組みで扱う時点でメンバが定義されていないということはないのです。

    これ以上となるとPHPの仕様レベルの話になりますので、PHPはオープンソースなので実際のコードを見てもらったほうが言葉で説明するよりも確実で間違いないかと思われます。

    蛇足ではありますが、PHPに限ったことではありませんがプログラミング言語というのは基本的にその言語を開発した言語が存在します。子の言語は親の言語で可能なことしかできません。

    キャンセル

+1

質問に対する本質的な回答ではないため参考情報として提示します。
ご存知だったら申し訳ないのですー。

PHP 7のRFCAbstract syntax treeに以下の記述がありました。

Implementation
Overview
The process for converting a PHP file into opcodes now consists of three phases:

Lexing: the generation of a token stream from the source code
Parsing: the generation of an abstract syntax tree from the token stream.
Compilation: the generation of op arrays from the abstract syntax tree.

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.12%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる