PHP 5.3版本跟随了很多新特性,其中比较惹眼的特性之一就是支持了闭包。文章将使用PHP 5.3 以及其他语言提供的闭包功能,用于展示如何“客观的”操作迭代数组。在开始之前先说明下,本例子仅仅是阐明观点,并没有考虑性能等其他方面的因素。
51CTO推荐专题: PHP开发基础入门
“货比三家”
用个简单的例子开始,有下面个数组:
- $nums = array(10, 20, 30, 40);
需要找出数组中大于15的项。那么,不考虑闭包的情况下,我们或许会这样写:
- $res = array();foreach ($nums as $n)
- {
- if ($n > 15)
- {
- $res[] = $n;
- }}
如果语言本身有闭包支持的,那么或许会这样写(Groovy 语言)
- def res = nums.findAll { it > 15 }
或者使用Scala语言:
- val res = nums filter (_ > 15)
译注:Javascript 1.6 的话会是如下:
- var res = nums.filter(function(c){return c > 15});
因为循环操作已被抽象起来,所以可以看到 Groovy 、Scala (以及 Javascript) 都很漂亮得用一行就可以搞定。当然,如果使用 PHP5.3 的闭包,也可以做到
- $res = array_filter($nums, function($v) { return $v > 15; });
PHP在这方面使用了比Scala更多的字符,但对比先前的例子,它更简短并且能更好得阅读。
顺便说下,上面的PHP代码实际上是使用了Lambda解析式,并不是个真正的闭包,这个并不是我们目前关注的重点。目前看来感觉都还不错,那么我们再的题目增加点难度:找到所有大于15的项, 然后乘以2再加上作用域中的的某个变量值以后再返回。
Groovy的实现:
- def x = 1def res = nums .findAll { it > 15 } .collect { it * 2 + x }
Scala的实现:
- val x = 1val res = nums filter (_ > 15) map (_ * 2 + x)
Javascript 的实现:
- var i = 1;var res = nums.filter(function(c)
- {return c > 15}).map(function(c){return c * 2 + i});
PHP的实现:
- $x = 1;$res = array_map(
- function($v) use ($x) { return $v * 2 + $x;
- }, array_filter( $nums, function($v) { return $v > 15; }
- ));
光从代码量方面,现在看起来PHP与其他语言有出入了。先抛开代码字面上本身的审美不谈,上面的PHP代码还有个额外的问题。例如,如果需要使用数组的键而非值作比较,怎么办?是的,上面的代码就办不到了。同时,从语法角度上说,上面的代码非常难以阅读。返璞归真,这时还是得返回老土的思路去解决问题:
- $x = 1;$res = array();foreach ($nums as $n)
- { if ($n > 15) { $res[] = $n * 2 + $x; }}
这样看起来又很清楚了。但这个时候你或许又会迷惑了:“那还瞎折腾啥,这不就是个数组操作吗?”。是的,好戏还在后头。这个时候该让 PHP 的某些高级特性出场,来搞定这看似有自残倾向 的“无聊问题”。
#p#
ArrayObject – 对数组的封装
PHP有个称作SPL的标准库,其中包含了个叫做ArrayObject的类,它能提供“像数组一 样操作类”的功能,例如
- $res = new ArrayObject(array(10, 20, 30, 40));foreach ($res as $v) {
- echo "$vn";}
ArrayObject是个内置的类,所以你可以像其他类类操作一样封装它。
Arr - 包上糖衣
既然我们已经有了ArrayObject以及闭包这些特性,我们就可以开始尝试封装它:
- class Arr extends ArrayObject
- {
- static function make($array)
- {
- return new self($array);
- } function map($func)
- {
- $res = new self();
- foreach ($this as $k => $v)
- {
- $res[$k] = $func($k, $v);
- }
- return $res;
- }
- function filter($func)
- {
- $res = new self();
- foreach ($this as $k => $v)
- {
- if ($func($k, $v))
- {
- $res[$k] = $v;
- }
- }
- return $res;
- }
- }
好了,万事俱备。下面重写的PHP代码就可以解决上面提到的问题,并且看起来语法上“差 不多”了:
- $res = Arr::make($nums)
- ->filter(function($k, $v) { return $v > 15; })
- ->map(function($k, $v) { return $v * 2; });
上面的代码与传统方式有何不同呢?首先,它们可以递归并形成作用链式的调用,因此可以 添加更多的类似操作。同时,可以通过回调的两个参数分别操作数组的键以及值其项 - $k 对应键以及 $v 对应值 。这使得我们可以在闭包中使用键值,这在传统的PHP函数 array_fliter中是无法实现的。
另外个带来的额外好处就是更加一致API调用。使用传统的 PHP 函数操作,它们有可能第一个参数是个闭包,或者是个数组,抑或是多个数组…总之谁知道呢?这里是 Arr 类的完整源代码,还包含了其他有用的函数(类似 reduce 以及 walk),其实它 们的实现其实方式和代码类似。
#p#
博弈
这个问题其实很难回答 - 这需要根据代码的上下文以及程序员自身等众多因素决定。其实 ,当我第一眼看见PHP的闭包实现时,我感觉似乎回到了那很久以前的Java时期,当时 我在开始使用匿名内置类(anonymous inner classes)来实现闭包。当然,这虽然可以做到, 但看起来实在是些画蛇添足。PHP 闭包本身是没错,只是它的实现以及语法让我感到非常的困惑。
其他具有闭包特性的语言,它们可以非常方便的调用闭包并同时具有优雅的语法。在上面的例子中,在 Scala中使用传统的循环也可以工作,但你会这样写吗?而从另个方面,那么有人 说上面这个题目使用 PHP 的闭包也可以实现,但一般情况下你会这样写吗?
可以确定,PHP 闭包在些情况下可以成为锐利的军刀(例如延时执行以及资源调用方面), 但在传统的迭代以及数组操作面前就显得有些为难。不要气馁不管怎么样, 返璞归真编写具有兼容性的、清爽的代码以及 API 是最重要的。
结束语
像所有后来加上的语法特性一样(记得当年Java的Generics特性不?以及前几年的PHP OOP特性),它们都需要时间磨合以及最终稳定下来。随着PHP5.3甚至将来的PHP 6逐渐普及,越来越多的技巧和特性相信在不远的将来逐渐的被聪明的程序员挖掘出来。回到最初文章开头那个题目,对比:
- $res = Arr::make($nums)
- ->filter(function($k, $v)
- { return $v > 15; })
- ->map(function($k, $v)
- { return $v * 2; });以及
- val res = nums filter (_ > 15) map (_ * 2)
两者之间的区别。归根结底它们仅是语法而已,本质上都是殊途同归解决了同个问题。程序语言的应用特性不同,自然孰优孰劣也就无从比较。最后,这里有此篇文章的代码示例, 相信可以找到更多如何使用PHP进行函数式迭代(当然不仅仅是这些)的心得。
文章转自李魁的博客,
原文地址:http://www.cahlk.com.cn/2010/984.shtml
【编辑推荐】