背景
最近有個朋友問我 PHP 命名空間是咋樣的,但是由於長期不做開發,筆者實際上也已經忘得差不多了,所以也回答不出來。只是記得和 Java 挺像的。事後重新查了一下 PHP 的官方文檔,並且和 Java 做對比,Java 的命名空間實際上來自於 JVM 本身的機制,JVM 是基於 class 位元組碼檔案載入類,由於類很容易出現重名的情況,換言之 class 位元組碼檔案也會出現重名情況,所以就需要使用目錄來管理不同的位元組碼檔案,而為了保證載入正常,所以就需要命名空間這種機制。當然,也可以說是由於命名空間的存在才有了目錄管理的方式。但是 PHP 和 Java 不一樣,PHP 是一種動態指令碼語言,它的代碼分散在所有指令碼中,當需要的時候才會使用 include函數載入對應的檔案,所以 PHP 的命名空間,實際上是基於 PHP 的自動載入類,自動載入類實現了才能保證 PHP 命名空間存在的意義。
命名空間概述
命名空間據筆者所知應該最早源於 C++ 語言,在 C++98 標準以後,為了保證各種命名不重合所推出的一種解決方案。現在的物件導向語言基本都有這種機制,當然除了命名空間以外,還有很多種方式,比如模組化,不過實際上這些機制都是用來解決封裝問題的,所以筆者個人認為並無好壞之分。先把 PHP 官方文檔代碼拉出來溜溜
非常容易理解的代碼,從上面的代碼中可以看到 PHP 定義的命名空間是怎麼樣的,不過筆者個人認為其定義非常反人類,居然使用反斜線來分隔命名空間路徑。不過有一點需要注意,名為 PHP 或 php 的命名空間,以及以這些名字開頭的命名空間(例如PHP\Classes)被保留用作語言核心使用,而不應該在使用者空間的代碼中使用。
定義命名空間
PHP 命名空間功能只能在 PHP5.3.0 以上版本使用,對於一個命名空間,只有類、介面、函數和常量會被包含在命名空間中。
當然,也可以使用花括弧來包含所有需要的內容,就像這樣。
不過這樣很容易造成縮排上的問題,所以筆者不推薦使用,並且一般情況下,一個檔案包含一個類,所以也不需要花括弧來分割命名空間範圍。
使用命名空間
對於命名空間路徑來說,存在著三種形式
-
非限定名稱,或者說不包含首碼的類名稱。例如 $a=new foo();或 foo::staticmethod();。如果當前命名空間是 currentnamespace, foo將被解析為 currentnamespace\foo。如果使用 foo 的代碼是全域的,不包含在任何命名空間中的代碼,則foo 會被解析為foo`。
-
限定名稱,或包含首碼的名稱,例如 $a = new subnamespace\foo();或 subnamespace\foo::staticmethod();。如果當前的命名空間是 currentnamespace,則 foo會被解析為 currentnamespace\subnamespace\foo。如果使用 foo的代碼是全域的,不包含在任何命名空間中的代碼, foo會被解析為 subnamespace\foo。
-
完全限定名稱,或包含了全域首碼操作符的名稱,例如, $a = new \currentnamespace\foo();或 \currentnamespace\foo::staticmethod();。在這種情況下, foo總是被解析為代碼中的文字名 (literal name)currentnamespace\foo。
由於 PHP 本身動態語言的特性,所以完全可以使用字串動態訪問命名空間內的元素。
不過有一點需要注意,就是單雙引號之間的區別,單引號可以不需要處理 \的轉譯處理,而雙引號就必須使用 \\等轉譯符號。
Java 語言使用 import 機制引入命名空間,由於 Java 可以指定到類名,所以 Java 最多隻能匯入到具體類,而 PHP 則可以指定到一個命名空間內的類、常量、方法等,並且支援命名空間別名。
名稱解析規則
首先就是前面講過的三種名稱類型,名稱解析遵循以下規則:
-
對完全限定名稱的函數,類和常量的調用在編譯時間解析。例如 new \A\B 解析為類 A\B。
-
所有的非限定名稱和限定名稱(非完全限定名稱)根據當前的匯入規則在編譯時間進行轉換。例如,如果命名空間 A\B\C 被匯入為 C,那麼對 C\D\e() 的調用就會被轉換為 A\B\C\D\e()。
-
在命名空間內部,所有的沒有根據匯入規則轉換的限定名稱均會在其前面加上當前的命名空間名稱。例如,在命名空間 A\B 內部調用 C\D\e(),則 C\D\e() 會被轉換為 A\B\C\D\e() 。
-
非限定類名根據當前的匯入規則在編譯時間轉換(用全名代替短的匯入名稱)。例如,如果命名空間 A\B\C 匯入為C,則 new C() 被轉換為 new A\B\C() 。
-
在命名空間內部(例如A\B),對非限定名稱的函數調用是在運行時解析的。例如對函數 foo() 的調用是這樣解析的:
-
在命名空間(例如A\B)內部對非限定名稱或限定名稱類(非完全限定名稱)的調用是在運行時解析的。下面是調用 new C() 及 new D\E() 的解析過程:
從上面的規則來看,實際上 PHP 的匯入規則和 Java 有點類似,但是卻有不一樣,主要是因為 Java 是完全物件導向的,而 PHP 本質上還只是一種基於對象的語言。
自動載入類
在早期 PHP 開發中,開發人員最煩的就是一堆 include 函數包含了一大堆檔案,而且早期時候 PHP 物件導向的概念確實太差了,因為 PHP 作為一種指令碼語言,不存在程式入口,所以指令碼順序化執行的誘惑力實在是很大,即使物件導向開發,但是缺少極佳的模組劃分匯入機制,代碼可以說很難有美感,最大的代表就是 Wordpress。如果有朋友看過這個典型項目,可以覺得非常痛苦,因為各種初始化、商務程序都分散在各個不同的檔案中,使用 include 函數進行銜接,然後每次頁面渲染都是同樣的要走一趟流程。當然,這是 Wordpress 的曆史包袱,而在支援老版本 PHP 的情況下 Wordpress 代碼已經寫得足夠最佳化了。
在 PHP5 中就不需要這麼麻煩了,因為可以定義一個 __autoload()函數,當調用一個未定義的類的時候就會啟動此函數,從而在拋出錯誤之前做最後的補救,不過這個函數的本意已經被完全曲解使用了,現在都用來做自動載入。
注意,這個函數實際上已經不被推薦使用了,相反,現在應當使用 spl_autoload_register()來註冊類的自動載入函數。
bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )
autoload_function 是需要註冊的自動裝載函數,如果此項為空白,則會註冊 spl_autoload函數,
throw 此參數設定了 autoload_function 無法成功註冊時, spl_autoload_register()是否拋出異常。
prepend 如果是 true, spl_autoload_register()會添加函數到隊列之首,而不是隊列尾部。
上面提到了 spl_autoload函數,實際上註冊函數的規範就應當遵循此函數,函式宣告如下:
void spl_autoload ( string $class_name [, string $file_extensions ] )
由於這個函數預設實現是通過 C 語言,所以這裡給出一個 PHP 語言的實現規範。
大致上就和這個是類似的。實際上命名空間和自動載入類的結合就基本是通過路徑形式
function __autoload(){ $dir = './libralies'; set_include_path(get_include_path(). PATH_SEPARATOR. $dir); $class = str_replace('\\', '/', $class) . '.php'; require_once($class);}
將命名空間路徑替換為實際路徑。