原文鏈接 by Patkos Csaba18 Apr 2013
反射通常被定義為一個程序在執(zhí)行的時候自我檢查和修改自身的邏輯的能力。用較少的專業(yè)術(shù)語來說,反射就是讓一個對象告訴你它自身的屬性與方法,并改變哪些成員(即使是私有的)。在這一課,我們將會升入了解是如何實現(xiàn)的,以及何時可能證明是有用的。
簡史
在編程時代的初期,只有匯編語言。用匯編語言編寫的程序駐留在電腦的物理寄存器中。通過讀取寄存器,可以隨時檢查其組成,方法和值。甚至,你可以在程序運行時通過修改簡單的修改寄存器來改變程序。這需要對正在運行的程序的有一些滲入的認知,但是這是底層的反射。
As with any cool toy, use reflection, but don't abuse it.
隨著高級編程語言(例如 C語言)的出現(xiàn),這種反射逐漸淡出并消失。后來它被重新引入了面向?qū)ο蟮某绦蛟O(shè)計中。
今天,絕大部分的語言都支持反射。靜態(tài)類型的語言,就像Java,極少有程序不使用反射的。然而,我發(fā)現(xiàn)有趣的是,所有的動態(tài)類型語言(例如PHP或者Ruby)都是基于反射的。如果沒有反射的概念,鴨子類型將不可能實現(xiàn)。當你傳遞一個對象給另一個人(參數(shù)),接收對象無法知道該對象的結(jié)構(gòu)和類型。它所能做的就是通過反射來標識可以和不能在接收的對象上調(diào)用的方法。
簡單舉例
反射在PHP中很流行。事實上,很多場景中你是用了反射但是你自己卻不知道。例如:
// Nettuts.php
require_once 'Editor.php';
class Nettuts {
function publishNextArticle() {
$editor = new Editor('John Doe');
$editor->setNextArticle('135523');
$editor->publish();
}
}
以及:
// Editor.php
class Editor {
private $name;
public $articleId;
function __construct($name) {
$this->name = $name;
}
public function setNextArticle($articleId) {
$this->articleId = $articleId;
}
public function publish() {
// publish logic goes here
return true;
}
}
在上述代碼中,我們直接調(diào)用了一個已知類型的且本地初始化的變量。很明顯我們在在publishNextArticle()方法中創(chuàng)建了editor變量,$editor變量就是Editor類型。這里并不需要反射,但是我們來使用一個新的類,命名為Manager:
// Manager.php
require_once './Editor.php';
require_once './Nettuts.php';
class Manager {
function doJobFor(DateTime $date) {
if ((new DateTime())->getTimestamp() > $date->getTimestamp()) {
$editor = new Editor('John Doe');
$nettuts = new Nettuts();
$nettuts->publishNextArticle($editor);
}
}
}
然后, 修改Nettuts文件, 如下:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle('135523');
$editor->publish();
}
}
現(xiàn)在Nettuts已經(jīng)很明顯與Editor類沒有任何關(guān)聯(lián)。它已經(jīng)不包含那個文件,它沒有初始化那個類也不知道它的存在。我能夠在publishNextArticle()方法中傳遞任何類型的對象且代碼能夠正常工作。

這個類就如你從上面的圖片中看到的,Nettus只有與Manager有一個直接的關(guān)聯(lián)關(guān)系。Manager創(chuàng)建了它,Manager依賴于Nettues。但是Nettuts與Editor類沒有任何關(guān)聯(lián)關(guān)系,且Editor也只是與Manager有關(guān)聯(lián)。
At runtime, Nettuts uses an Editor object, thus the <<uses>> and the question mark.在運行時,PHP檢查接收的對象且檢驗它實現(xiàn)的setNextArticle()方法和publish()方法。
對象成員信息
我們可以是PHP顯示一個對象的詳情。我們創(chuàng)建一個PHP單元測試類來幫助我們更方便的演練我們的代碼:
// ReflectionTest.php
require_once '../Editor.php';
require_once '../Nettuts.php';
class ReflectionTest extends PHPUnit_Framework_TestCase {
function testItCanReflect() {
$editor = new Editor('John Doe');
$tuts = new Nettuts();
$tuts->publishNextArticle($editor);
}
}
現(xiàn)在,在Nettuts中添加一個var_dump()方法:
// Nettuts.php
class NetTuts {
function publishNextArticle($editor) {
$editor->setNextArticle('135523');
$editor->publish();
var_dump(new ReflectionClass($editor));
}
}
運行單元測試類,然后觀察輸出結(jié)果中的神奇顯:
PHPUnit 3.6.11 by Sebastian Bergmann.
.object(ReflectionClass)#197 (1) {
["name"]=>
string(6) "Editor"
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
我們的反射類有一個屬性名為name值為$editor變量的原始類型:Editor,但是這里并沒有多少信息。我們的Editor的方法呢?
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle('135523');
$editor->publish();
$reflector = new ReflectionClass($editor);
var_dump($reflector->getMethods());
}
}
在這段代碼中,我們將反射類的實例復(fù)制給$reflector變量,現(xiàn)在我們可以很方便的處罰它的方法了。反射類公開了許多用于獲取對象信息的方法。這其中一個方法就是getMethods(),它能返回一個包含每個方法的信息的數(shù)組。
PHPUnit 3.6.11 by Sebastian Bergmann.
.array(3) {
[0]=>
&object(ReflectionMethod)#196 (2) {
["name"]=>
string(11) "__construct"
["class"]=>
string(6) "Editor"
}
[1]=>
&object(ReflectionMethod)#195 (2) {
["name"]=>
string(14) "setNextArticle"
["class"]=>
string(6) "Editor"
}
[2]=>
&object(ReflectionMethod)#194 (2) {
["name"]=>
string(7) "publish"
["class"]=>
string(6) "Editor"
}
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
其他的方法,getProperties(),檢索對象的屬性(即使是私有屬性!):
PHPUnit 3.6.11 by Sebastian Bergmann.
.array(2) {
[0]=>
&object(ReflectionProperty)#196 (2) {
["name"]=>
string(4) "name"
["class"]=>
string(6) "Editor"
}
[1]=>
&object(ReflectionProperty)#195 (2) {
["name"]=>
string(9) "articleId"
["class"]=>
string(6) "Editor"
}
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
從getMethod()以及getProperties()方法中返回的數(shù)組中的元素的類型分別是反射方法以及反射屬性;這些對象都是相當有用的:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle('135523');
$editor->publish(); // first call to publish()
$reflector = new ReflectionClass($editor);
$publishMethod = $reflector->getMethod('publish');
$publishMethod->invoke($editor); // second call to publish()
}
}
這里我們使用getMethod()方法去檢索一個名為"publish"的方法;獲取到的結(jié)果就是一個反射方法類型的對象。然后我們調(diào)用invoke()方法,通過傳遞$editor對象再一次執(zhí)行editor的publish()方法。
在我們的例子中,這個過程很簡單,因為我們已經(jīng)有一個Editor對象傳遞給invoke()方法。我們可能需要幾個Editor對象在一些情景中,給予我們豐富的對象去選擇使用。在其他的情景中,我們可能沒有相應(yīng)的對象可以使用,在這種情況下我們將會需要從反射類中獲取一個對象。
讓我們修改Editor的publish()方法去演示兩次調(diào)用:
// Editor.php
class Editor {
[ ... ]
public function publish() {
// publish logic goes here
echo ("HERE\n");
return true;
}
}
以及新的輸出信息:
PHPUnit 3.6.11 by Sebastian Bergmann.
.HERE
HERE
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
操縱實例數(shù)據(jù)
我們也可以在代碼執(zhí)行的時候進行修改。那如果修改沒有公共設(shè)置器的私有變量 呢?我們在Editor里面添加一個方法去檢索editor的name變量:
// Editor.php
class Editor {
private $name;
public $articleId;
function __construct($name) {
$this->name = $name;
}
[ ... ]
function getEditorName() {
return $this->name;
}
}
這是一個行的方法被調(diào)用,getEditorName(),且只是簡單的返回了從私有變量$name中獲取的值。$name變量在構(gòu)建時就被賦值了,且我們沒有公開的方法讓我們?nèi)バ薷乃5俏覀兛梢酝ㄟ^反射來訪問這個變量。你可能首先嘗試更明顯的方法:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty('name');
$editorName->getValue($editor);
}
}
即使它在var_dump()行有輸出值,它還是拋出了一個錯誤當我們嘗試通過反射檢索值時:
PHPUnit 3.6.11 by Sebastian Bergmann.
Estring(8) "John Doe"
Time: 0 seconds, Memory: 2.50Mb
There was 1 error:
1) ReflectionTest::testItCanReflect
ReflectionException: Cannot access non-public member Editor::name
[...]/Reflection in PHP/Source/NetTuts.php:13
[...]/Reflection in PHP/Source/Tests/ReflectionTest.php:13
/usr/bin/phpunit:46
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
為了解決這個問題,我們需要使用反射屬性對象授權(quán)我們使用私有變量以及方法:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty('name');
$editorName->setAccessible(true);
var_dump($editorName->getValue($editor));
}
}
調(diào)用setAccessible()方法且傳遞true標志:
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) "John Doe"
string(8) "John Doe"
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
如你所看到的,我們設(shè)法去讀取私有變量。第一行輸出數(shù)據(jù)是來自對象自身的getEditorName()方法,接著就是來自于反射。但是修改私有變量呢?使用setValue()方法:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty('name');
$editorName->setAccessible(true);
$editorName->setValue($editor, 'Mark Twain');
var_dump($editorName->getValue($editor));
}
}
就是這樣。這段代碼把"John Doe"改為"Mark Twain"。
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) "John Doe"
string(10) "Mark Twain"
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
間接反射的使用
PHP的一些內(nèi)置功能間接使用單一反射通過調(diào)用call_user_func()這個方法。
回調(diào)
call_user_func()方法接收一組數(shù)組:第一個元素指向一個對象,第二個是方法的名稱。你可以提供一個可選參數(shù),然后將其傳遞給被調(diào)用函數(shù)。例如:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty('name');
$editorName->setAccessible(true);
$editorName->setValue($editor, 'Mark Twain');
var_dump($editorName->getValue($editor));
var_dump(call_user_func(array($editor, 'getEditorName')));
}
}
以下輸出表明代碼檢索到的數(shù)據(jù)正確:
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) "John Doe"
string(10) "Mark Twain"
string(10) "Mark Twain"
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
使用變量的值
關(guān)于間接反射的另外一個例子是通過變量的值調(diào)用方法,與直接調(diào)用相反。例如:
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty('name');
$editorName->setAccessible(true);
$editorName->setValue($editor, 'Mark Twain');
var_dump($editorName->getValue($editor));
$methodName = 'getEditorName';
var_dump($editor->$methodName());
}
}
這段代碼生成了與前一段代碼一樣的輸出。PHP簡單地用它代表的字符串替換變量并調(diào)用該方法。當你想通過使用類名的變量來創(chuàng)建對象時,它仍然可以工作。
我們何時需要使用反射?
現(xiàn)在我們已經(jīng)了解了技術(shù)的細節(jié),我們需要思考何時才去使用反射?這里有少許的場景:
- 如果沒有反射 動態(tài)類型 將會不可能實現(xiàn)。
- 面向方面編程 從方法調(diào)用中偵聽并放置代碼在方法周圍,所有這些都通過反射完成。
- PHPUnit 極其依賴反射,與其他模擬框架一樣。
- Web框架 通常使用反射用于不同的目的。一些框架將反射用于初始化模型,構(gòu)造視圖對象以及更多的東西。Laravel在依賴注入中大量的使用了反射。
- 元編程,就像我們最后一個例子,這是一個隱藏反射。
- 代碼分析框架 使用反射來理解你的代碼。
最后思考
和任何酷的玩具一樣,使用反射,但不要濫用它。當你檢查許多對象時,反射時昂貴的,它有可能使您的項目的架構(gòu)和設(shè)計復(fù)雜化。我建議你在反射能真正給你帶來優(yōu)勢或者你沒有任何其他可行選擇的時候使用它。
就個人而言,我只在幾個示例中使用反射,最常見的是使用缺少文檔的第三方模塊。當你的MVC響應(yīng)包含一個方法"add"和"remove"值得變量時,很方便調(diào)用正確的方法。