原文链接 by Patkos Csaba18 Apr 2013
反射通常被定义为一个程序在执行的时候自我检查和修改自身的逻辑的能力。用较少的专业术语来说,反射就是让一个对象告诉你它自身的属性与方法,并改变哪些成员(即使是私有的)。在这一课,我们将会升入了解是如何实现的,以及何时可能证明是有用的。
简史
在编程时代的初期,只有汇编语言。用汇编语言编写的程序驻留在电脑的物理寄存器中。通过读取寄存器,可以随时检查其组成,方法和值。甚至,你可以在程序运行时通过修改简单的修改寄存器来改变程序。这需要对正在运行的程序的有一些渗入的认知,但是这是底层的反射。
As with any cool toy, use reflection, but don't abuse it.
随着高级编程语言(例如 C语言)的出现,这种反射逐渐淡出并消失。后来它被重新引入了面向对象的程序设计中。
今天,绝大部分的语言都支持反射。静态类型的语言,就像Java,极少有程序不使用反射的。然而,我发现有趣的是,所有的动态类型语言(例如PHP或者Ruby)都是基于反射的。如果没有反射的概念,鸭子类型将不可能实现。当你传递一个对象给另一个人(参数),接收对象无法知道该对象的结构和类型。它所能做的就是通过反射来标识可以和不能在接收的对象上调用的方法。
简单举例
反射在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;
}
}
在上述代码中,我们直接调用了一个已知类型的且本地初始化的变量。很明显我们在在publishNextArticle()方法中创建了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();
}
}
现在Nettuts已经很明显与Editor类没有任何关联。它已经不包含那个文件,它没有初始化那个类也不知道它的存在。我能够在publishNextArticle()方法中传递任何类型的对象且代码能够正常工作。
这个类就如你从上面的图片中看到的,Nettus只有与Manager有一个直接的关联关系。Manager创建了它,Manager依赖于Nettues。但是Nettuts与Editor类没有任何关联关系,且Editor也只是与Manager有关联。
At runtime, Nettuts uses an Editor object, thus the <<uses>> and the question mark.在运行时,PHP检查接收的对象且检验它实现的setNextArticle()方法和publish()方法。
对象成员信息
我们可以是PHP显示一个对象的详情。我们创建一个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);
}
}
现在,在Nettuts中添加一个var_dump()方法:
// Nettuts.php
class NetTuts {
function publishNextArticle($editor) {
$editor->setNextArticle('135523');
$editor->publish();
var_dump(new ReflectionClass($editor));
}
}
运行单元测试类,然后观察输出结果中的神奇显:
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());
}
}
在这段代码中,我们将反射类的实例复制给$reflector变量,现在我们可以很方便的处罚它的方法了。反射类公开了许多用于获取对象信息的方法。这其中一个方法就是getMethods(),它能返回一个包含每个方法的信息的数组。
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()方法中返回的数组中的元素的类型分别是反射方法以及反射属性;这些对象都是相当有用的:
// 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"的方法;获取到的结果就是一个反射方法类型的对象。然后我们调用invoke()方法,通过传递$editor对象再一次执行editor的publish()方法。
在我们的例子中,这个过程很简单,因为我们已经有一个Editor对象传递给invoke()方法。我们可能需要几个Editor对象在一些情景中,给予我们丰富的对象去选择使用。在其他的情景中,我们可能没有相应的对象可以使用,在这种情况下我们将会需要从反射类中获取一个对象。
让我们修改Editor的publish()方法去演示两次调用:
// 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)
操纵实例数据
我们也可以在代码执行的时候进行修改。那如果修改没有公共设置器的私有变量 呢?我们在Editor里面添加一个方法去检索editor的name变量:
// Editor.php
class Editor {
private $name;
public $articleId;
function __construct($name) {
$this->name = $name;
}
[ ... ]
function getEditorName() {
return $this->name;
}
}
这是一个行的方法被调用,getEditorName(),且只是简单的返回了从私有变量$name中获取的值。$name变量在构建时就被赋值了,且我们没有公开的方法让我们去修改它。但是我们可以通过反射来访问这个变量。你可能首先尝试更明显的方法:
// 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.
为了解决这个问题,我们需要使用反射属性对象授权我们使用私有变量以及方法:
// 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));
}
}
调用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)
如你所看到的,我们设法去读取私有变量。第一行输出数据是来自对象自身的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的一些内置功能间接使用单一反射通过调用call_user_func()这个方法。
回调
call_user_func()方法接收一组数组:第一个元素指向一个对象,第二个是方法的名称。你可以提供一个可选参数,然后将其传递给被调用函数。例如:
// 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')));
}
}
以下输出表明代码检索到的数据正确:
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)
使用变量的值
关于间接反射的另外一个例子是通过变量的值调用方法,与直接调用相反。例如:
// 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简单地用它代表的字符串替换变量并调用该方法。当你想通过使用类名的变量来创建对象时,它仍然可以工作。
我们何时需要使用反射?
现在我们已经了解了技术的细节,我们需要思考何时才去使用反射?这里有少许的场景:
- 如果没有反射 动态类型 将会不可能实现。
- 面向方面编程 从方法调用中侦听并放置代码在方法周围,所有这些都通过反射完成。
- PHPUnit 极其依赖反射,与其他模拟框架一样。
- Web框架 通常使用反射用于不同的目的。一些框架将反射用于初始化模型,构造视图对象以及更多的东西。Laravel在依赖注入中大量的使用了反射。
- 元编程,就像我们最后一个例子,这是一个隐藏反射。
- 代码分析框架 使用反射来理解你的代码。
最后思考
和任何酷的玩具一样,使用反射,但不要滥用它。当你检查许多对象时,反射时昂贵的,它有可能使您的项目的架构和设计复杂化。我建议你在反射能真正给你带来优势或者你没有任何其他可行选择的时候使用它。
就个人而言,我只在几个示例中使用反射,最常见的是使用缺少文档的第三方模块。当你的MVC响应包含一个方法"add"和"remove"值得变量时,很方便调用正确的方法。