很多語言都提供了名稱空間特性,包括 C++ 和 Java™ 編程語言。引入名稱空間是為了幫助組織大型的代碼庫,因為在大型代碼庫中,應用程序經常會出現函數名或類名重疊問題,這會引起其他問題。使用名稱空間可以幫助識別代碼提供的函數或實用程序,甚至可以幫助指定其來源。一個例子就是 C# 中的 System 名稱空間,它包含有 .NET 框架提供的所有函數和類。
在其他未提供正式名稱空間的語言中(比如 PHP V5.2 以及更早版本),人們常常通過在類或函數名中使用特定的命名約定來發揮名稱空間的作用。比如 Zend Framework,其中每個類名以 Zend 開頭,並且每個子名稱空間使用下劃線分隔開。比如,類定義 Zend_Db_Table 表示 Zend Framework 中的一個類並且提供資料庫功能。這種方法的一個缺點就是產生的代碼非常繁瑣,尤其是那些包含好幾層的類或函數(Zend Framework 中的 Zend_Cache_Backend_Apc 就是一個例子)。另一個問題就是所有代碼必須遵循這種風格,因此如果在應用程序中集成了不遵循這種命名約定的第三方代碼后,問題就複雜了。
PHP 名稱空間的發展也並非一帆風順。它們最初計劃引入到 PHP V5 中,但是由於無法獲得恰當的實現,因此在開發階段被放棄。最後決定將它們併入到 PHP V6 中,在 2007 年決定將所有 nonunicode 增強移到另一個 PHP V5.x 發行版后,名稱空間隨後被移入到 PHP V5.3 中。儘管自最初的設計之後絕大部分名稱空間行為沒有發生變化,但是使用哪一種操作符卻成了最大的問題,並且社區成員對這個問題有不同的看法。2008 年 10 月最終決定使用反斜杠作為操作符,從而解決了所有在語言設計和適用性方面使用各種其他操作符的問題。
PHP 名稱空間
PHP 從其他語言中借鑒了很多名稱空間的語法和設計 — 最突出的是 C++。然而,PHP 名稱空間在某些方面具有自己的獨特性,這對於希望像在其他語言中那樣使用名稱空間的用戶來說是一個挑戰。在本節中,我們將研究 PHP 名稱空間的工作方式。
定義一個名稱空間
定義一個新的名稱空間非常簡單。要定義新名稱空間,在一個文件中添加清單 1 中的代碼作為第一個命令或輸出。
<?php namespace Foo; class Example {} ?> |
注意,以上 namespace 的聲明必須是文件中的第一個命令或輸出。在它的前面添加任何內容都會導致一個致命的錯誤。清單 2 展示了有關這方面的一些例子。
/* File1.php */ <?php echo "hello world!"; namespace Bad; class NotGood {} ?> /* File2.php */ <?php namespace Bad; class NotGood {} ?> |
在清單 2 的第 1 部分中,我們嘗試在名稱空間定義之前回傳到控制台,這導致產生一個致命錯誤。在清單的第 2 部分中,我們在 <?php 打開標記的前面多加了一個空格,這樣也導致一個致命錯誤。在編寫自己的代碼時一定要注意這種情況,因為這是 PHP 名稱空間中很常見的一種錯誤。
但是,上面的兩個例子都可以重新編寫,將名稱空間定義和將在名稱空間聲明中放入的代碼放到獨立的文件中,然後再將此文件包含到原始文件中。清單 3 演示了這一點。
/* Good.php */ <?php namespace Good; class IsGood() {} ?> /* File1.php */ <?php echo "hello world!"; include './good.php'; ?> /* File2.php */ <?php include './good.php'; ?> |
現在我們已經了解了如何在一個文件中定義代碼的名稱空間,接下來讓我們看看如何在應用程序中利用這個使用名稱空間的代碼。
使用帶有名稱空間的代碼
定義了名稱空間並在其中放入代碼后,我們就可以在應用程序中方便地使用它。可以使用很多種方法調用帶有名稱空間的函數、類或常量。一種方式是顯式地將名稱空間引用為調用的前綴。另一種方法是為名稱空間定義一個別名並使用該別名作為調用的前綴,這樣做的目的是簡化名稱空間前綴。最後,我們可以只在代碼中使用名稱空間,這就使它成為默認名稱空間,並且在默認情況下,使所有代碼都引用默認名稱空間。清單 4 演示了調用之間的不同之處。
/* Foo.php */ <?php namespace Foo; function bar() { echo "calling bar...."; } ?> /* File1.php */ <?php include './Foo.php'; Foo/bar(); // outputs "calling bar...."; ?> /* File2.php */ <?php include './Foo.php'; use Foo as ns; ns/bar(); // outputs "calling bar...."; ?> /* File3.php */ <?php include './Foo.php'; use Foo; bar(); // outputs "calling bar...."; ?> |
清單 4 演示了在名稱空間 Foo 內調用函數 bar() 的不同方法。在 File1.php 內,我們看到了如何進行顯式調用,使用名稱空間的名稱作為調用前綴。File2.php 使用名稱空間名稱的別名,因此我們使用別名代替名稱空間的名稱。最後,File3.php 僅使用名稱空間,這允許我們不需要使用任何前綴來調用 bar()。
我們還可以在一個文件內定義多個名稱空間,只需要在文件中添加更多 namespace 調用。清單 5 演示了這一點。
<?php namespace Foo; class Test {} namespace Bar; class Test {} $a = new Foo\Test; $b = new Bar\Test; var_dump($a, $b); Output: object(Foo\Test)#1 (0) { } object(Bar\Test)#2 (0) { } |
現在我們已經基本了解了如何在名稱空間內進行調用,讓我們了解一些更複雜的名稱空間調用以及它們如何工作。
名稱空間解析
要熟悉名稱空間的使用,其中一個難點就是了解如何進行範圍解析。儘管清單 4 所示的簡單例子是合理的,但是當我們開始對名稱空間進行彼此嵌套時,或者在一個名稱空間中試圖針對全局空間發出調用是,就會出現問題。PHP V5.3 提供了可以以合理的方式自動解決這些問題的規則。
讓我們創建一些包含(include)文件,每個文件都定義了函數 hello()。
/* global.php */ <?php function hello() { echo 'hello from the global scope!'; } ?> /* Foo.php */ <?php namespace Foo; function hello() { echo 'hello from the Foo namespace!'; } ?> /* Foo_Bar.php */ <?php namespace Foo/Bar; function hello() { echo 'hello from the Foo/Bar namespace!'; } ?> |
清單 6 在三個不同範圍內對 hello() 函數定義了三次:在全局範圍內,在 Foo 名稱空間中,在 Foo/Bar 名稱空間中。根據發出 hello() 函數調用的範圍,決定對哪個 hello() 函數執行調用。下面展示了這些調用的例子。在這裡,我們將使用 Foo 名稱空間查看如何在另一個名稱空間中調用 hello() 函數。
<?php include './global.php'; include './Foo.php'; include './Foo_Bar.php'; use Foo; hello(); // outputs 'hello from the Foo namespace!' Bar\hello(); // outputs 'hello from the Foo/Bar namespace!' \hello(); // outputs 'hello from the global scope!' ?> |
可以看到,在當前名稱空間內引用子名稱空間時,可以縮短名稱空間前綴(Foo/Bar/hello() 調用可被縮短為 Bar/hello())。並且我們看到如何指定以在全局空間內調用方法:只需使用名稱空間操作符作為調用的前綴。
現在,我們已經了解了名稱空間的工作機制,下面我們將查看如何在自己的代碼中使用它們。
PHP 名稱空間用例
名稱空間的總體目標就是幫助我們更好地組織代碼,減少全局空間內的定義數量。在本節中,我們將查看一些例子,看看名稱空間如何幫助我們輕鬆地實現這些目標。
使用名稱空間的第三方代碼
許多 PHP 應用程序使用來自不同來源的代碼,包括像 PEAR 庫那樣經過精心設計的代碼,或者來自 CakePHP 或 Zend Framework 等各種框架的代碼,或是來自 Internet 上不同位置的代碼。在集成這些代碼時,最主要的問題之一就是這些代碼可能無法恰當地融合到已有代碼中;函數或類名可能與應用程序中已經在使用的內容沖?。
其中一個例子就是 PEAR Date 包。它使用類名 Date,這是一個非常通用的類名,並且可以很好地切入到代碼中的其他位置。因此,一個良好的解決方法就是在包內部的 Date.php 文件的頂部添加一個簡單的名稱空間命令。現在,當希望使用 PEAR Date 類而不是我們自己的 PEAR Date 類時,就不會感到迷惑。
<?php require_once('PEAR/Date.php'); use PEAR\Date; // the name of the namespace we've specified in PEAR/Date.php // since the current namespace is PEAR\Date, we don't need to prefix the namespace name $now = new Date(); echo $now->getDate(); // outputs the ISO formatted date // this example shows the full namespace specified as part of the class name $now = new PEAR\Date\Date(); echo $now->getDate(); // outputs the ISO formatted date ?> |
我們已經在 PEAR/Date.php 文件的 PEAR/Date 名稱空間內定義了 PEAR Date 類,因此現在只需在我們的文件中包含代碼並使用名稱空間,或使用名稱空間的名稱作為類或函數名的前綴。通過這種方法,我們就可以安全地在應用程序中包含第三方代碼。
名稱衝突不僅僅是第三方代碼才有的問題。如果大型代碼庫的各個部分永遠不會互相靠近,那麼也會出現此問題。在下一小節中,我們將了解名稱空間如何應對這個情況。
避免實用函數名衝突
幾乎所有 PHP 應用程序都具有大量實用方法。雖然並非應用程序的任意對象都包含實用方法,並且也不一定存在於應用程序的所有部分,但是總的來說它在應用程序中確實發揮著作用。但是,隨著應用程序不斷壯大,實用方法會引起維護問題。
其中一個產生問題的位置就是單元測試,我們編寫代碼來測試運行應用程序的代碼。大多數單元測試套件被設計為運行整個測試套件中的所有測試。比如,我們有兩個永遠不會包含在一起的實用方法文件,但是在測試套件中,它們就會包含在一起,因為我們會一次性測試整個應用程序。儘管使用這種方式設計應用程序不利於長期維護,但是它確實存在於大型遺留代碼庫中。
清單 9 展示了如何避免這一問題。我們有兩個文件 utils_left.php 和 utils_right.php,這是面向主要使用右手的用戶和主要使用左手的用戶的實用函數集合。對於每個文件,我們在其各自的名稱空間內分別定義。
/* utils_left.php */ <?php namespace Utils\Left; function whichHand() { echo "I'm using my left hand!"; } ?> /* utils_right.php */ <?php namespace Utils\Right; function whichHand() { echo "I'm using my right hand!"; } ?> |
我們定義了一個 whichHand() 函數,函數的輸出表示我們使用哪一隻手。在清單 10 中,我們看到可以方便地包含兩個文件並在希望調用的名稱空間之間進行切換。
<?php include('./utils_left.php'); include('./utils_right.php'); Utils\Left\whichHand(); // outputs "I'm using my left hand!" Utils\Right\whichHand(); // outputs "I'm using my right hand!" use Utils\Left; whichHand(); // outputs "I'm using my left hand!" use Utils\Right; whichHand(); // outputs "I'm using my right hand!" |
現在,兩個文件可以安全地包含在一起,並且我們指定了處理函數調用所需使用的名稱空間。而且,對現有代碼的影響很小,因為重構功能只需要我們在文件的頂部添加 use 語句,表示要使用的名稱空間。
可以對我們定義的 PHP 代碼進一步擴展。在下一小節,我們將了解如何在名稱空間內覆蓋內部函數。
覆蓋內部函數名稱
雖然 PHP 的內部函數經常可以提供非常棒的實用方法,但有時它們不能按照我們期望的那樣執行。我們需要增強它們的行為,以使函數符合我們的期望,但是我們也需要使用另一個名字重新定義函數,從而避免進一步混淆範圍。
文件系統函數就需要我們執行這些操作。假設我們需要確保 file_put_contents() 創建的任何文件具有某些許可權集。比如,假設我們希望這些文件的許可權為只讀;我們可以使用一個新的名稱空間重新定義函數,如下所示。
<?php namespace Foo; function file_put_contents( $filename, $data, $flags = 0, $context = null ) { $return = \file_put_contents( $filename, $data, $flags, $context ); chmod($filename, 0444); return $return; } ?> |
我們在函數內調用內部 file_put_contents() 函數並使用一個反斜杠作為函數名的前綴,表示該函數應當在全局範圍內處理,這表示將調用內部函數。調用了內部函數后,我們隨後對文件執行 chmod() 命令來設置相應的許可權。
還有許多例子可以演示如何使用名稱空間增強代碼。在任何情況下,我們應避免執行不恰當的修改,比如將函數名或類名作為前綴以生成獨特的名稱。我們現在還了解了如何使用名稱空間在大型應用程序中更加安全地包含第三方代碼,同時不需要擔心名稱衝突。
結束語
PHP V5.3 的名稱空間是該語言中一個非常受歡迎的新增特性,可以幫助開發人員合理地組織應用程序的代碼。該特性使您能夠避免使用標準來處理名稱空間,允許您編寫更高效的代碼。儘管名稱空間的出現經歷了很長時間,但對於受名稱衝突困擾的大型 PHP 應用程序來說,它是一個非常受歡迎的特性。(責任編輯:A6)
[火星人 ] PHP V5.3 中的新特性,第 3 部分: 名稱空間已經有625次圍觀