闭包函数和 lambda 函数绝对不是新出现的概念;它们均来自函数编程领域。函数编程 是一种编程风格,它将关注点从执行命令转移到表达式计算。这些表达式是使用函数构成的,结合这些函数可以得到我们要查找的结果。这种编程风格最常用于学术目的,但是也可以在人工智能与数学领域中见到,并且可以在用 Erlang、Haskell 及 Scheme 等语言编写的商业应用程序中找到。
闭包 最初是在 20 世纪 60 年代作为 Scheme 的一部分开发的,Scheme 是最著名的函数编程语言之一。Lambda 函数和闭包通常出现在允许将函数处理为第一类值(First-class value)的语言中,这意味着函数可以动态创建并作为参数传递给其他语言。
从那时起,闭包及 lambda 函数已经找到了走出函数编程世界并进入 JavaScript、Python 和 Ruby 等语言的方法。JavaScript 是支持闭包和 lambda 函数的最常见语言之一。JavaScript 实际使用这些函数作为支持面向对象的编程方法,把函数嵌套到其他函数中以用作私有成员。清单 1 提供了 JavaScript 如何使用闭包的示例。
清单 1. 使用闭包构建 JavaScript 对象
var Example = function()
{
this.public = function()
{
return "This is a public method";
};
var private = function()
{
return "This is a private method";
};
};
Example.public() // returns "This is a public method"
Example.private() // error - doesn't work
|
如清单 1 中所示,Example 对象的成员函数被定义为闭包。由于私有方法作用于局部变量(与绑定到使用此关键字的 Example 对象的公共方法相反),因此从外部看不到它。
现在我们已经了解了这些概念的历史,让我们查看 PHP 中的 lambda 函数。lambda 函数的概念是闭包的基础,并且提供了一种比 PHP 中已有的 create_function() 函数改进了很多的动态创建函数的方法。
Lambda 函数
Lambda 函数(或者通常所谓的 “匿名函数”)是可以随时定义的简单抛弃型函数,并且通常都与变量绑定。函数本身仅存在于定义函数的变量范围内,因此当该变量超出范围时,函数也超出范围。lambda 函数的理念源于 20 世纪 30 年代的数学研究。它被称为 lambda 演算,用于研究函数定义与应用程序以及递归概念。lambda 演算用于开发函数编程语言,例如 Lisp 和 Scheme。
对于大多数实例来说,尤其对于接受回调函数的许多 PHP 函数来说,Lambda 函数非常方便。array_map() 就是这样一种函数,它允许我们遍历数组并将回调函数应用到数组的每个元素上。在早期版本的 PHP 中,这些函数的最大问题是没有一种清晰的方式定义回调函数;我们坚持使用以下三种解决方法的其中一种:
- 我们可以在代码中的其他位置定义回调函数,因此我们知道它可用。这有些麻烦,因为它把调用的实现部分移到了其他位置,这样做对于可读性与可维护性极为不便,尤其是不打算在其他位置使用此函数时。
- 我们可以在同一个代码块中定义回调函数,但是使用一个名称。虽然这样做有助于把内容放在一起,但是需要在定义周围添加
if 块以避免名称空间冲突。清单 2 展示了这种方法。
清单 2. 在同一个代码块中定义指定的回调
function quoteWords()
{
if (!function_exists ('quoteWordsHelper')) {
function quoteWordsHelper($string) {
return preg_replace('/(\w)/','"$1"',$string);
}
}
return array_map('quoteWordsHelper', $text);
}
|
- 我们可以使用
create_function() (从 V4 开始就是 PHP 的一部分)在运行时创建函数。虽然在功能上此函数执行了所需操作,但是它有一些缺点。一个主要缺点是,它在运行时而非编译时编译,不允许操作码缓存来缓存函数。它的语法智能(syntax-wise)也非常糟糕,并且大多数 IDE 中的字符串高亮显示功能完全不起作用。
虽然接受回调函数的函数功能十分强大,但是没有一种好方法可以执行一次性回调函数,而无需执行一些非常笨拙的工作。使用 PHP V5.3,我们可以使用 lambda 函数以更规则的方法重新执行以上示例。
清单 3. 使用 lambda 函数用于回调的 quoteWords()
function quoteWords()
{
return array_map('quoteWordsHelper',
function ($string) {
return preg_replace('/(\w)/','"$1"',$string);
});
}
|
我们看到了定义这些函数的更规则的语法,这可以通过操作码缓存来优化性能。我们也已经得到了改进的可读性以及与字符串高亮显示功能的兼容性。让我们在此基础上了解如何在 PHP 中使用闭包。
回页首
闭包
Lambda 函数本身并没有添加以前不能执行的功能。正如我们所见,我们可以使用 create_function() 执行所有这项操作,尽管后果是使用更糟糕的语法并且性能更加不理想。但是它们仍然是抛弃型函数并且不维护任何类型的状态,这限制了我们可以用它们执行的操作。因此出现了闭包并使 lambda 函数得到增强。
闭包是在它自己的环境中执行计算的函数,它有一个或多个绑定变量可以在调用函数时访问。它们来自函数编程世界,其中涉及大量概念。闭包类似于 lambda 函数,但是在与定义闭包的外部环境中的变量进行交互方面更加智能。
让我们看一看如何在 PHP 中定义闭包。清单 4 显示了从外部环境导入变量并将其简单地输出到屏幕上的闭包示例。
清单 4. 简单闭包示例
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
Output:
Hello World!
|
从外部环境中导入的变量是在闭包函数定义的 use 子句中指定的。默认情况下,它们是由值传递的,意味着如果要更新传递到闭包函数定义内的值,则不更新外部值。但是,我们可以通过在变量前放置 & 运算符来完成此操作,这种方法在函数定义中用于表示按引用传递。清单 5 显示了这种方法的示例。
清单 5. 通过引用传递变量的闭包
$x = 1
$closure = function() use (&$x) { ++$x; }
echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";
Output:
1
2
3
|
可以看到,闭包使用外部变量 $x 并在每次调用闭包时递增该变量。我们可以将按值和按引用传递的变量轻松地混合到 use 子句中,并且可以顺利地处理这些变量。我们也可以拥有直接返回闭包的函数,如清单 6 所示。在本例中,闭包的生命周期实际上比定义闭包的方法长。
清单 6. 函数返回的闭包
function getAppender($baseString)
{
return function($appendString) use ($baseString) { return $baseString .
$appendString; };
}
|
回页首
闭包与对象
闭包不但是过程式编程的有用工具,而且是面向对象编程的有用工具。在这种情况下使用闭包与在类外部使用闭包实现的目的相同:包含在小范围内绑定的特定函数。在对象外部与在对象内部使用一样简单。
在对象内定义时,非常方便的一点是闭包通过 $this 变量拥有对对象的完全访问权,而无需显式导入。清单 7 演示了该示例。
清单 7. 对象内的闭包
class Dog
{
private $_name;
protected $_color;
public function __construct($name, $color)
{
$this->_name = $name;
$this->_color = $color;
}
public function greet($greeting)
{
return function() use ($greeting) {
echo "$greeting, I am a {$this->_color} dog named
{$this->_name}.";
};
}
}
$dog = new Dog("Rover","red");
$dog->greet("Hello");
Output:
Hello, I am a red dog named Rover.
|
在这里,我们在闭包内显式使用提供给 greet() 方法的欢迎词,闭包在该方法内定义。我们还在闭包内获取狗的颜色和名字,传递到构造函数中并存储到对象中。
在类中定义的闭包基本上与在对象外部定义的闭包相同。惟一的不同之处在于通过 $this 变量自动导入对象。我们可以通过将闭包定义为静态闭包禁用此行为。
清单 8. 静态闭包
class House
{
public function paint($color)
{
return static function() use ($color) { echo "Painting the
house $color...."; };
}
}
$house = new House();
$house->paint('red');
Output:
Painting the house red....
|
此示例类似于清单 5 中定义的 Dog 类。最大的差别是在闭包内不使用对象的任何属性,因为它被定义为静态类。
在对象内使用静态闭包与使用非静态闭包相比的最大优点是节省内存。由于无需将对象导入闭包中,因此可以节省大量内存,尤其是在拥有许多不需要此功能的闭包时。
针对对象的另一个优点是添加名为 __invoke() 的魔术方法,此方法允许对象本身被调用为闭包。如果定义了此方法,则在该上下文中调用对象时将使用此方法。清单 9 演示了示例。
清单 9. 使用 __invoke() 方法
class Dog
{
public function __invoke()
{
echo "I am a dog!";
}
}
$dog = new Dog();
$dog();
|
将清单 9 中所示的对象引用调用为变量将自动调用 __invoke() 魔术方法,使类本身用作闭包。
闭包可以很好地与面向对象的代码以及面向过程的代码整合。让我们看一看闭包如何与 PHP 的强大 Reflection API 交互。
回页首
闭包与反射
PHP 有一个有用的反射 API,它允许我们对类、接口、函数和方法执行反向工程。按照设计,闭包是匿名函数,这意味着它们不显示在反射 API 中。
但是,新 getClosure() 方法已经添加到 PHP 中的 ReflectionMethod 和 ReflectionFunction 类中,可以从指定的函数或方法动态创建闭包。它在此上下文中用作宏,通过闭包调用函数方法将在定义它的上下文中执行函数调用。清单 10 显示了此方法如何运行。
清单 10. 使用 getClosure() 方法
class Counter
{
private $x;
public function __construct()
{
$this->x = 0;
}
public function increment()
{
$this->x++;
}
public function currentValue()
{
echo $this->x . "\n";
}
}
$class = new ReflectionClass('Counter');
$method = $class->getMethod('currentValue');
$closure = $method->getClosure()
$closure();
$class->increment();
$closure();
Output:
0
1
|
这种方法的一个有趣的副作用是允许通过闭包访问类的私有成员和受保护成员,这有利于对类执行单元测试。清单 11 展示了对类的私有方法的访问。
清单 11. 访问类的私有方法
class Example
{
....
private static function secret()
{
echo "I'm an method that's hiding!";
}
...
}
$class = new ReflectionClass('Example');
$method = $class->getMethod('secret');
$closure = $method->getClosure()
$closure();
Output:
I'm an method that's hiding!
|
此外,可以使用反射 API 来内省(introspect)闭包本身,如清单 12 所示。只需将对闭包的变量引用传递到 ReflectionMethod 类的构造函数中。
清单 12. 使用反射 API 内省闭包
$closure = function ($x, $y = 1) {};
$m = new ReflectionMethod($closure);
Reflection::export ($m);
Output:
Method [ <internal> public method __invoke ] {
- Parameters [2] {
Parameter #0 [ <required> $x ]
Parameter #1 [ <optional> $y ]
}
}
|
关于向后兼容性值得注意的一点是,PHP 引擎现在保留类名 Closure 并用于存储闭包,因此使用该名称的所有类都需要重命名。
正如我们所见,反射 API 能够通过现有函数和方法动态创建闭包,从而为闭包提供强大的支持。它们还可以像普通函数一样内省到闭包中。
回页首
为什么使用闭包?
如在 lambda 函数的示例中所示,闭包的最明显用法之一是少数 PHP 函数接受回调函数作为参数。但是,在需要把逻辑封装到自己的范围内的情况下,闭包会十分有用。重构旧代码以进行简化并提高可读性就是这样一个例子。查看以下示例,该示例显示了在运行一些 SQL 查询时使用的记录程序。
清单 13. 记录 SQL 查询的代码
$db = mysqli_connect("server","user","pass");
Logger::log('debug','database','Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
Logger::log('debug','database','Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
Logger::log('debug','database','Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
Logger::log('debug','database','Insert Saw into to parts table');
|
从清单 13 中可以看出执行操作的重复程度。对 Logger::log() 执行的每次调用都有相同的前两个实参。为了解决此问题,我们可以把该方法调用放入闭包并转而针对该闭包执行调用。得到的代码如下所示:
清单 14. 记录 SQL 查询的重构代码
$logdb = function ($string) { Logger::log('debug','database',$string); };
$db = mysqli_connect("server","user","pass");
$logdb('Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
$logdb('Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
$logdb('Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
$logdb('Insert Saw into to parts table');
|
代码不但在外观上更加规则,而且更易于更改 SQL 查询日志的日志级别,因为现在只需要在一个位置进行更改。
回页首
结束语
本文演示了闭包在 PHP V5.3 代码中作为函数编程构造时多么有用。我们讨论了 lambda 函数及闭包与这些函数相比的优点。对象与闭包可以很好地结合使用,比如我们在面向对象的代码内对闭包的特殊处理。我们看到了如何使用反射 API 创建动态闭包,以及如何内省现有的闭包。
|
请发表评论