通過類名動態生成物件

tlanyan發表於2019-03-03

轉載請註明文章出處:tlanyan.me/dynamic-new…

問題

前幾天有人在PHP的QQ群裡問生成物件的問題:

use A\B;
$b = new B();  // 正確
$str = "B";
$b = new $str(); // 錯誤,提示:類"B"未找到
複製程式碼

類似問題五六年前碰到過,因此印象深刻。熱心提示要用 "完全限定類名" 形式,可惜連說兩遍,提問題的人都沒理解我說的(或者認為我的回覆與其問題無關):

問題解答

不得已下,寫下示範程式碼並 @ 提問題的人,終於讓其明白:

程式碼解答

原理

問題解決了,背後的原理是什麼?

從人的角度看,程式碼意圖非常明顯:動態生成類B的例項。但從執行引擎的角度,完全是另外一回事。其實new $classname()背後的運作行為類似於:

// 虛擬碼
if (class_exists($str)) {
  $b = new $str();
  return $b;
}
throw ClassNotFoundException;

// 或者用反射
try {
  $reflectionClass = new ReflectionClass($str);
  $b = $reflectionClass.newInstance();
  return $b;
}
throw ClassNotFoundException;
複製程式碼

要根據類名動態生成示例,首先要判斷類是否存在吧?PHP中與之相關的是class_exists函式和ReflectionClass類。在上面的例子中,只傳入字串 "B"class_exists回返回true嗎?

答案是否定的。class_existsReflectionClass只會在全域性類列表中根據名字查詢,不會理會呼叫函式所在(或引入)的名字空間。同理,如果使用use引入類名並做別名(as),別名類在class_exists中也會返回false

那麼PHP能否改進一下class_existsReflectionClass的行為,讓其根據當前上下文判斷?

可以這麼做,但是代價很大,原因包括:

  1. class_existsReflectionClass都沒有指示程式上下文Context的引數;

  2. PHP比較坑的一點:類名不會像函式、常量一樣往上逐級查詢;

  3. 如果存在多個同名的類,載入哪個?如以下程式碼所示:

    示例程式碼

    不管採取哪種行為,都會招致吐槽。

保持目前的情況,除動態生成例項時需要完全限定類名,並無其他槽點。並且實現上簡單,行為明確且一致。

總結

作為一門指令碼語言,PHP非常的靈活,但也會帶來一些使用上的困惑。本文所討論的根據類名動態生成物件,就要無視當前所在或引入的名字空間,必須使用完全限定類名形式。

作為對比,C++不能動態生成物件。Java要用Class.forName的方式獲取class物件,然後再呼叫建構函式生成。Java不能直接new類名,避免了PHP中的坑,但Class.forName同樣需要完全限定類名,避免不明確行為。

參考

  1. PHP回顧之反射
  2. PHP中的過載
  3. Using namespaces: fallback to global function/constant

相關文章