PHP Closure(闭包)类详解

 Closure

面向对象变成语言代码的复用主要采用继承来实现,而函数的复用,就是经过闭包来实现。这就是闭包的设计初衷。php

 

注:PHP里面闭包函数是为了复用函数而设计的语言特性,若是在闭包函数里面访问指定域的变量,使用use关键字来实现。编程

 

PHP具备面向函数的编程特性,可是也是面向对象编程语言,PHP 会自动把闭包函数转换成内置类 Closure 的对象实例,依赖Closure 的对象实例又给闭包函数添加了更多的能力。 数组

 

闭包不能被实例(私有构造函数),也不能被继承(finally 类)。能够经过反射来判断闭包实例是否能被实例,继承。闭包

 

匿名函数

匿名函数,是php5.3的时候引入的,又称为Anonymous functions。字面意思也就是没有定义名字的函数。编程语言


提到闭包就不得不想起匿名函数,也叫闭包函数(closures),貌似PHP闭包实现主要就是靠它。声明一个匿名函数是这样:
函数

 

$say = function() {
  return '我是匿名函数';
}; //带结束符

echo $say(); //这是最直接调用匿名函数方式

function test(Closure $callback){
  return $callback();
}

echo test($say); //这是间接调用匿名函数方式










 固然也能够这样写 :
  echo test( function() {
  return '我是匿名函数';
  });


this

  

能够看到,匿名函数由于没有名字,若是要使用它,须要将其返回给一个变量。匿名函数也像普通函数同样能够声明参数,调用方法也相同:spa

$func = function( $param ) {
    echo $param;
};
$func( 'some string' );

//输出:
//some string

  

顺便提一下,PHP在引入闭包以前,也有一个能够建立匿名函数的函数:create function,可是代码逻辑只能写成字符串,这样看起来很晦涩而且很差维护,因此不多有人用。.net

 

实现闭包


将匿名函数在普通函数中当作参数传入,也能够在函数中被返回。这就实现了一个简单的闭包。
设计

 

链接闭包和外界变量的关键字:USE


PHP在默认状况下,匿名函数不能调用所在代码块的上下文变量,而须要经过使用use关键字。

function getMoney() {
    $rmb = 1;
    $func = function() use ( $rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb; //闭包内的变量改变了,可是闭包外没有改变。
}
getMoney();

//输出:
//1
//1

 

在上面咱们看见匿名函数不能改变上下文的变量,是由于use所引用的也只不过是变量的一个副本clone而已(非彻底引用变量自己)。

若是咱们想在匿名函数中改变上下文的变量呢?想要彻底引用变量,而不是复制呢?要达到这种效果,在变量前加一个 & 符号便可。

 

function getMoney() {
    $rmb = 1;
    $func = function() use ( &$rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb;
}
getMoney();

//输出:
//1
//2

 

好,这样匿名函数就能够引用上下文的变量了。若是将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能获得这些变量,这样造成‘闭包’这个概念可能会更清晰一些。

根据描述咱们再改变一下上面的例子:

function getMoneyFunc() { $rmb = 1; $func = function() use ( &$rmb ) { echo $rmb.'<br>'; //把$rmb的值加1
        $rmb++; }; return $func; } $getMoney = getMoneyFunc(); //匿名函数返回给外界,保存了$rmb的变量 $getMoney(); $getMoney(); $getMoney(); //输出: //1 //2 //3

从例子中理解“将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能获得这些变量”,将匿名函数返回给外界后,$rmb这个引用变量就被保存了,提高成了全局变量(全局变量,相似静态变量),然后面每次再调用匿名函数,传入的都是这个保存后的值

 

 

总结:

闭包函数不能直接访问闭包外的变量,而是经过use 关键字来调用上下文变量(闭包外的变量),也就是说经过use来引用上下文的变量;

闭包内所引用的变量不能被外部所访问(即,内部对变量的修改,外部不受影响),若想要在闭包内对变量的改变从而影响到上下文变量的值,须要使用&的引用传参。

 use所引用的是变量的复制(副本而),并非彻底引用变量。若是要达到引用的效果,就须要使用 & 符号,进行引用传递参数;

 

若是咱们要调用一个类里面的匿名函数呢?直接上demo

<?php class A { public static function testA() { return function($i) { //返回匿名函数
            return $i+100; }; } } function B(Closure $callback) { return $callback(200); } $a = B(A::testA()); print_r($a);//输出 300

其中的A::testA()返回的就是一个无名funciton。

 

绑定的概念

上面的例子的Closure只是全局的的匿名函数,好了,那咱们如今想指定一个类有一个匿名函数。也能够理解说,这个匿名函数的访问范围再也不是全局的了,而是一个类的访问范围。

那么咱们就须要将“一个匿名函数绑定到一个类中”。

 

PHP Closure 类是用于表明匿名函数的类,匿名函数(在 PHP 5.3 中被引入)会产生这个类型的对象,Closure类摘要以下:

Closure {
    __construct ( void )
    public static Closure bind (Closure $closure , object $newthis [, mixed $newscope = 'static' ])
    public Closure bindTo (object $newthis [, mixed $newscope = 'static' ])
}

 

参数说明:

closure
须要绑定的匿名函数。

newthis
须要绑定到匿名函数的对象,或者 NULL 建立未绑定的闭包。

newscope
想要绑定给闭包的类做用域,或者 'static' 表示不改变。若是传入一个对象,则使用这个对象的类型名。 类做用域用来决定在闭包中 $this 对象的 私有、保护方法 的可见性。(备注:能够传入类名或类的实例,默认值是 'static', 表示不改变。)

返回值
返回一个新的 Closure 对象 或者在失败时返回 FALSE

 

方法说明:

 

Closure::__construct — 用于禁止实例化的构造函数
Closure::bind — 复制一个闭包,绑定指定的$this对象和类做用域。
Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类做用域。

除了此处列出的方法,还有一个 __invoke 方法。这是为了与其余实现了 __invoke()魔术方法 的对象保持一致性,但调用闭包对象的过程与它无关。

 

Closure::bind是Closure::bindTo的静态版本

 


深刻理解bind参数:

要理解bing参数,先要理解类属性的可访问权限 public,private(子类不可访问),protect(子类可访问,类外不能够访问)。
php的public、protected、private三种访问控制模式的区别

public: 公有类型 在子类中能够经过self::var调用public方法或属性,parent::method调用父类方法
在实例中能够能过$obj->var 来调用 public类型的方法或属性
protected: 受保护类型在子类中能够经过self::var调用protected方法或属性,parent::method调用父类方法
在实例中不能经过$obj->var 来调用 protected类型的方法或属性
private: 私有类型该类型的属性或方法只能在该类中使用,在该类的实例、子类中、子类的实例中都不能调用私有类型的属性和方法

不能经过 $this 访问静态变量,静态方法里面也不可用 $this (缘由:静态属性属于类自己而不属于类的任何实例。静态属性能够被看作是存储在类当中的全局变量,能够在任何地方经过类来访问它们)
在类外不能经过 类名::私有静态变量,只能在类里面经过self,或者static 访问私有静态变量:

第一个参数:须要绑定的匿名函数。
第二个参数:关于bind的第二个参数为object仍是null,取决于第一个参数闭包中是否用到了`$this`的上下文环境。(绑定的对象决定了函数中的$this的取值)
若闭包中用到了`$this`,则第2个参数不可为null,只能为object实例对象;若闭包中用到了静态访问(::操做符),第2个参数就能够为null,也能够为object
第三个参数(做用域):是控制闭包的做用域的,若是闭包中访问的是 private 属性,就须要第3个参数提高闭包的访问权限,若闭包中访问的是public属性,第三个参数能够不用。只有须要改变访问权限时才要,传对象,类名均可以。

















  [bind的第三个参数:mixed类型的类做用域,决定了这个匿名函数中可以调用哪些私有和保护的方法。也就是说this能够调用的方法,即这个this能够调用的方法,即这个this能够调用的方法与属性,与这个scope一致。
  第三个参数若是是类实例对象与类的名称都表明着这个闭包有类做用域,若是是static则表示这个闭包与外部变量做用域同样,不能访问类的私有以及保护方法。
  mixed类型在PHP中也就是没有类型限定的意思,缺省状况下第三个参数是个字符串,值是‘static’]




   第2个参数是给闭包绑定`$this`对象的,第3个参数是给闭包绑定做用域的。

经过成员的可访问行来举例子理解:

class A{ private $name = '王力宏'; protected $age = '30'; private static $weight = '70kg'; public $address = '中国'; public static $height = '180cm'; }


 

 

$obj = new A(); //echo $obj->name;//报错 Cannot access private property A::$name //echo $obj->age;//报错 Cannot access protected property A::$age //echo A::$weight; //报错 Cannot access private property A::$weight
  echo $obj->address;//正常 输出 中国
  echo A::$height;//正常 输出 180cm
  $fun = function(){ $obj = new A(); return $obj->address;//实例对象能够得到公有属性, $obj->name等私有属性确定不行 上面例子已列出报错
 } echo $fun();//正常 输出 中国
  $fun2 = function(){ return A::$height;// 类能够直接访问公有静态属性,但A::$weight确定不行,由于weight为私有属性
 } echo $fun2();//正常 输出 180cm

 

以上都理解的状况下 咱们来看看这样的状况,有以下匿名函数:

$fun = function(){ return $this->name; } 或者 $fun = function(){ return A::$height; } echo $fun();//会报错的

其实单独这段代码是确定不能运行 调用的,由于里面有个$this,程序压根不知道你这个$this是表明那个对象 或 那个类(而且就算知道那个对象或类,该对象是否拥有name属性,若是没有照样会有问题)

所以想让其正常运行确定有前提条件啊(就比如你想遍历某个数组同样,若是这个数组压根你就没提早定义 声明 确定会报错的)
若是这样:

$fun = function(){ //name是私有属性,须要第3个参数
    return $this->name; } ; $name = Closure::bind($fun,new A() ,'A'); echo $name();//输出 王力宏 
该函数返回一个全新的 Closure匿名的函数,和原来匿名函数$fun如出一辙,只是其中$this被指向了A实例对象,这样就能访问address属性了。

使用了bind函数后 ,

实际上是匿名函数里的$this被指定到了或绑定到了A实例对象上了

Closure::bind($fun,new A() ); 这个使用 你能够理解成对匿名函数作了以下过程:
$fun = function(){ $this = new A(); return $this->name; } ;

你能够想象认为,$this就成了A类的实例对象了,而后在去访问name属性,就和咱们正常实例化类访问成员属性同样,上面2中的例子$obj = new A()就是这样,(由于$this是关键字,在这里咱们其实不能直接$this = new A();这么写,为了好理解我写成$this,可是原理仍是这个意思),可是咱们都知道由于name属性是私有的,上面2中我已说过,实例对象不能访问私有属性,那该怎么办呢,因而添加第三个参数就很重要了,通常传入传入一个对应对象,或对应类名(对应的意思是:匿名函数中$this-name想获取name属性值,你这个$this想和那个类和对象绑定在一块儿呢,就是第二个参数,这时你第三个参数写和第二个参数写同样的对象或类就好了,就是做用域为这个对象或类,这就会让原理的name私有属性变为公有属性)

$fun = function(){ //address为公开,第3个参数能够不须要
    return $this->name; } ; $address = Closure::bind($fun,new A()); echo $address();

这里数一下bind此次为何没添加第三个参数,由于咱们要访问的address属性是公有的,一个对象实例是能够直接访问公有属性的,这个例子中只要匿名函数中$this被指向了A对象实例(或者叫绑定也能够),就能访问到公有属性,因此能够不用添加第三个参数,固然你加上了第三个参数 如这样Closure::bind($fun2, new A(), ‘A’ );或Closure::bind($fun2, new A(), nwe A() );不影响 照样运行,就比如把原来公有属性 变为公有属性 不影响的(通常当咱们访问的属性为私有属性时,才使用第三个参数改变做用域 ,使其变为公有属性)

$fun = function() { //访问私有静态属性,访问静态属性,第2个参数能够为null,访问私有属性,须要第3个参数提升做用域
  //return A::$weight;  这样写,会报错,提示不能经过类名访问私有静态变量
  //weight 为类的私有属性,只可在类中访问
  return self::$weight;

 }; //echo $fun(); 运行会报错 由于weight为私有属性 。 $weight = Closure::bind($fun,null,'A'); //经过bind函数做用,返回一个和$fun匿名函数如出一辙的匿名函数,只是该匿名函数中A::$weight, weight属性由私有变成公有属性了。 echo $weight();


为何第二个参数又成null了呢,由于在该匿名函数中A::$weight 这属于正常类使用啊(php中 类名::公有静态属性,这是正常访问方法,上面2中例子已经说的很清楚了),因此不用绑定到某个对象上去了,因而第二个参数能够省略,惟一遗憾的是weight属性虽是静态属性,可是其权限是private私有属性,因而咱们要把私有属性变公有属性就能够了,这时把第三个参数加上去就能够了,第三个参数能够是A类(Closure::bind($fun,null,'A')),也能够是A类的对象实例(Closure::bind($fun,null, new A() )),两种写法均可以,最终第三个参数的添加使私有属性变成了公有属性,(这个例子中固然你非得添加第二个参数确定也没问题,只要第二个参数是A的实例对象就行Closure::bind($fun,new A(),'A'),不影响,只是说 A::$weight 这种使用方法自己就是正常使用,程序自己就知道你用的是A类,你在去把它指向到A类本身的对象实例上,属于画蛇添足,所以第二个参数加不加都行,不加写null就行)

 

综上你们应该理解其用法了吧,有时第二个参数为null,有时第三个参数能够不要,这些都跟你匿名函数里 代码中访问的方式紧密相关
总结:
一、通常匿名函数中有$this->name相似这样用 $this访问属性方式时,你在使用bind绑定时 ,第二个参数确定要写,写出你绑定那个对象实例,第三个参数要不要呢,要看你访问的这个属性,在绑定对象中的权限属性,若是是private,protected 你要使用第三个参数 使其变为公有属性, 若是原本就是公有,你能够省略,也能够不省略
二、通常匿名函数中是 类名::静态属性 相似这样的访问方式(好比例子中A::$weight),你在使用bind绑定时,第二个参数能够写null,也能够写出具体的对象实例,通常写null就行(写了具体对象实例画蛇添足),第三个参数写不写仍是得看你访问的这个静态属性的权限是 private 仍是 public,若是是私有private或受保护protected的,你就得第三个参数必须写,才能使其权限变为公有属性 正常访问,若是原本就是公有public能够不用写,能够省略


三、须要提高属性做用域时,第3个参数须要传,传对象或者类名均可以。

 

例子:

class Animal {
    public $cat = 'cat';
    public static $dog = 'dog';
    private $pig = 'pig';
    private static $duck = 'duck';
}

//不能经过 $this 访问静态变量
//不能经过 类名::私有静态变量,只能经过self,或者static,在类里面访问私有静态变量

$cat = function() {
    return $this->cat;
};

$dog = static function () {
    return Animal::$dog;
};

$pig =  function() {
    return $this->pig;
};

$duck = static function() {
    //return Animal::$duck;  这样写,会报错,提示不能经过类名访问私有静态变量
  return self::$duck; // return static::$duck
};

$bindCat = Closure::bind($cat, new Animal(), 'Animal');
$bindCat2 = Closure::bind($cat, new Animal(), new Animal());
echo $bindCat() . PHP_EOL;
echo $bindCat2() . PHP_EOL;

$bindDog = Closure::bind($dog, null, 'Animal');
$bindDog2 = Closure::bind($dog, null, new Animal());
echo $bindDog() . PHP_EOL;
echo $bindDog2() . PHP_EOL;

$bindPig = Closure::bind($pig, new Animal(), 'Animal');
$bindPig2 = Closure::bind($pig, new Animal(), new Animal());
echo $bindPig() . PHP_EOL;
echo $bindPig2() . PHP_EOL;

$bindDuck = Closure::bind($duck, null, 'Animal');
$bindDuck2 = Closure::bind($duck, null, new Animal());
echo $bindDuck() . PHP_EOL;
echo $bindDuck2() . PHP_EOL;

 

经过上面的例子,能够看出函数复用得,能够把函数挂在不一样的类上,或者对象上

 

闭包函数的应用:

/** * 一个基本的购物车,包括一些已经添加的商品和每种商品的数量 * */
class Cart { // 定义商品价格
  const PRICE_BUTTER = 10.00; const PRICE_MILK = 30.33; const PRICE_EGGS = 80.88; protected $products = array(); /** * 添加商品和数量 * * @access public * @param string 商品名称 * @param string 商品数量 */
  public function add($item, $quantity) { $this->products[$item] = $quantity; } /** * 获取单项商品数量 * * @access public * @param string 商品名称 */
  public function getQuantity($item) { return isset($this->products[$item]) ? $this->products[$item] : FALSE; } /** * 获取总价 * * @access public * @param string 税率 */
  public function getTotal($tax) { $total = 0.00; $callback = function ($quantity, $item) use ($tax, &$total) { $pricePerItem = constant(__CLASS__ . "::PRICE_" . strtoupper($item)); //调用以上对应的常量
          $total += ($pricePerItem * $quantity) * ($tax + 1.0); }; array_walk($this->products, $callback); return round($total, 2); } } $my_cart = new Cart; // 往购物车里添加商品及对应数量
$my_cart->add('butter', 10); $my_cart->add('milk', 3); $my_cart->add('eggs', 12); // 打出出总价格,其中有 3% 的销售税.
echo $my_cart->getTotal(0.03);//输出 1196.4

 

 给类的私有属性赋值:

class A { private $attr = array(); } class B { public static function bind (A $a) { return \Closure::bind(function () use ($a) { //给私有属性赋值
            $a->attr = [ 'color' => 'red',
                'weight' => '1.0', ]; }, null, A::class); } } //咱们在对象外部给私有对象赋值时,须要经过\Closure::bind ,来提升闭包做用域进行赋值
$a = new A(); //不能对私有属性直接进行$a->attr = ['color' => 'red','weight' => '1.0'];
call_user_func(B::bind($a)); print_r($a);

 

 经过上面的几个例子,其实匿名绑定的理解就不难了....咱们在看一个扩展的demo(引入trait特性)

给类动态的添加方法

官方文档有例子,点击查看

 

/** * 给类动态添加新方法 * * @author fantasy */ trait DynamicTrait { /** * 自动调用类中存在的方法 */
    public function __call($name, $args) { if(is_callable($this->$name)){ return call_user_func($this->$name, $args); }else{ throw new \RuntimeException("Method {$name} does not exist"); } } /** * 添加方法 */
    public function __set($name, $value) { $this->$name = is_callable($value)?
            $value->bindTo($this, $this):
            $value; } } /** * 只带属性不带方法动物类 * * @author fantasy */
class Animal { use DynamicTrait; private $dog = '汪汪队'; } $animal = new Animal; // 往动物类实例中添加一个方法获取实例的私有属性$dog
$animal->getdog = function() { return $this->dog; }; echo $animal->getdog();//输出 汪汪队

 

获取类属性:第3个参数 score 范围 设置为null 时,只能获取到 public 属性。

若是但愿彻底取消绑定,则须要将闭包和范围都设置为null

 

class MyClass { public $foo = 'a'; protected $bar = 'b'; private $baz = 'c'; /** * @param bool $all 是否获取所有的属性 * * @return array */
    public function toArray($all = false) { // Only public variables
        $callback = (function ($obj) { // get_object_vars — 返回由对象属性组成的关联数组 ,在类内调用时,会返回全部的属性(private,protect,public), 在类外使用只返回public
            return get_object_vars($obj); }); //指定做用域 为 null 则会返回 public 属性; 指定做用域默认 ‘static’,则会返回全部属性
        if ($all) { $callback = $callback->bindTo(null); } else { //类内获取public属性
            $callback = $callback->bindTo(null, null); } return $callback($this); } } $obj = new MyClass; $vars = get_object_vars($obj); // get_object_vars 类外使用,获取到的是 public 属性
$vars = $obj->toArray();

 

 

总结:

1. 闭包内若是用 $this, 则 $this 只能调用非静态的属性,这和实际类中调用原则是一致的,且 Closure::bind() 方法的第2个参数不能为null,必须是一个实例 (由于$this,必须在实例中使用),第三个参数能够是实例,能够是类字符串,或 static;

2. 闭包内调用静态属性时,闭包必须声明为 static,同时Closure::bind()方法的第2个参数须要为null,由于 静态属性不须要实例,第3个参数能够是类字符串,实例,static.

相关文章
相关标签/搜索