php匿名函数的起源,使用.lambda,closure类使用,bind,call切换作用域.

定义

    // 在js中常见的自运行函数
    (function(){
        echo 'hello';
    })();

    // 赋值
    $func1=function(){};

    // 箭头函数
    $func2=fn()=>echo 'hello';

    // 参数使用
    $arr=[1,2,3];
    array_walk($arr,function($v,$k){
        echo $v;
    });

    // use 作用域
    $a="hello";
    $func3=function() use($a){
        echo $a;
    };

函数参数

在<php5.3前没匿名函数以前怎么传函数.
php中传递函数参数有两种:

  1. 函数名字符串
  2. create_function

通过函数名传参

$a="hello";
function callback(){
    global $a;
    echo $a;
}

/**
 * 接收函数的参数
 * @param callable $callback
 * @return void
 */
function foo(callable $callback){
    var_dump(is_string($callback)); // true
    var_dump(is_callable($callback)); // true
    var_dump($callback instanceof Closure);// false

    var_dump($callback); // "callback"
    $callback();
}

foo('callback');

如上例子,传递的是字符串"callback"接受却是callable类型.
开发调试时很难确定是普通字符串还是函数,且只使用一次却需要定义一个函数.

在php7.2以前还有一个函数create_function 动态创建函数.
如上代码改成.

$a='hello';
function foo(callable $callback){
    var_dump(is_string($callback)); // true
    var_dump(is_callable($callback)); // true
    var_dump($callback instanceof Closure);// false

    var_dump($callback); // "?lambda_1"
    $callback();
}

foo(create_function('','global $a;echo $a;'));

虽然代码结构清淅了,可以看出生成代码比较复杂,容易写错. 内部使用 eval 执行的.性能和安全都有问题.

在PHP中函数使用的是字符串描述.

可以看出来 create_function 返回的否是普通的有名函数.

了解 create_function

$func=create_function('','echo __FUNCTION__;');
$func(); // __lambda_func
var_dump($func);//lambda_1"

实际上create_function 创建出来的还是有名函数.
只是这个函数名很怪.

能不能直接运行呢?

$func=create_function('','echo __FUNCTION__;');

var_dump(function_exists('lambda_1')); // false
lambda_1();// 不存在

注意实际名称前有一段乱码.
-w255

实际上php利用特殊字符命名函数,生成用户无法输入的唯一函数.这里使用的是ascii的0值.
规则: null+lambda_n
php中的大量类似的黑科技,属于奇淫技巧很不规范.
php的函数变量名规范是 [A-z0-9_]

$func=create_function('','echo __FUNCTION__;');
$fn=chr(0)."lambda_1";
$fn();

实际上还是有名函数.
函数创建时就会在内存中,会依赖eval.

为了支持真正的匿名函数和闭包,php5.3后支持匿名函数.

匿名函数

lambda 实际是一个对象.
而不在用字符串描述函数.
同时引入了__invoke魔术方法.
匿名函数实际就是Closure实例.

$cb=function(...$args){
    echo 'hello';
};
$cb();

相当于 $cb=new Closure();

相当于下面代码:

final class Lambda {
    protected $callable;

    private function __construct(){}

    public function __invoke(...$args)
    {
       return call_user_func_array($this->callable,$args);
    }

    public static function fromCallable(callable $cb):self{
        $obj= new self();
        $obj->callable=$cb;
        return $obj;
    }
}


$callback=Lambda::fromCallable(create_function('','echo "hello";'));
$callback();
var_dump($callback);
var_dump(is_callable($callback));
var_dump($callback instanceof Lambda);

实现closure类.

闭包

$cb=function(){};
// 相当于
$cb= new Closure;

php中的闭包不能直接获取作用域变量. 需要手动那 use.
use 更多只读.

$name="张三";
$foo=function() use(&$name){
    echo $name;
    $name="李四";
};

// global 读写
//$foo();// 张三
//$foo();// 李四
//echo $name; // 李四

// use 只读
//$foo();// 张三
//$foo();// 李三
//echo $name; // 张三

// &use 读写
//$foo();// 张三
//$foo();// 李四
//echo $name; // 张四

路由使用closure示例:

class App{

    protected $routes=[];
    protected $responseStatus='200 OK';
    protected $responseContentType='text/html';
    protected $responseBody='index';

    public function addRoute(string $path,callable $callback){
        $this->routes[$path]=$callback->bindTo($this,$this);
    }

    public function dispatch($path){
        if(isset($this->routes[$path]) && is_callable($this->routes[$path])){
            $this->routes[$path]();
        }

        header('HTTP/1.1 ' . $this->responseStatus);
        header('Content-Type: ' . $this->responseContentType);
        header('Content-Length: ' . mb_strlen($this->responseBody));
        echo $this->responseBody;
    }
}

$app = new App();
$app->addRoute('/',function(){});
$app->addRoute('/hello',function(){
    $this->responseBody='hello';
});

$app->dispatch('/hello');

这里面用到了匿名函数和bindTo.
可以看到匿名函数里可以调用$this.

BindTo

绑定$this对象和作用域.生成一个新的函数.
https://www.php.net/manual/zh/closure.bindto.php

从 PHP 5.4 开始,在类里面使用匿名函数时,匿名函数的 $this 将自动绑定到当前类.

class App {
    public function run(){
        return  function(){
            // PHP 5.4 开始,可以直接使用$this
            var_dump($this); // App
            var_dump(self::class); //App
        };
    }
}

$app= new App;
$callback=$app->run();
$callback();

如果不想绑定$this,使用 static 静态匿名函数.

class App {

    public function run(){
        return static function(){
            var_dump($this); // error
            var_dump(self::class); //App
        };
    }
}

$app= new App;
$callback=$app->run();
$callback();

普通匿名函数不绑定$this.
通过bindTo修改$this指向A.

class App {

    public function run(){
        return  function(){
            var_dump($this); // A
            var_dump(self::class); //A
        };
    }
}

class A{}
$obj = new A;
$app= new App;
$callback=$app->run();
$cb=$callback->bindTo($obj,A::class);
$cb();

移除和$this和作用域.

$cb=$callback->bindTo(null,null);

只指向this.

class A{}
$obj = new A;
$cb=$callback->bindTo(obj,null);

只修改到A的作用域.

class A{}
$cb=$callback->bindTo(null,A::class);

如果设为static静态函数,则不能指向this.

除了bindTo,还有同名函数bind

Closure::bind

用法和func->bindTo一样.

class App {
    public function run(){
        return  function(){
            var_dump($this); // A
            var_dump(self::class); //A
        };
    }
}

class A{}
$obj = new A;
$app= new App;
$callback=$app->run();
//$cb=$callback->bindTo($obj,A::class);
$cb=Closure::bind($callback,$obj,A::class);
$cb();

Call函数

call和bind的区别,call可以直接调用,bind还需要另调用.
call临时修改this,而不是复制生成函数.
相当于 call_user_func($cb,[1,2,3])
call(newThis,args);

class App {

    public function run(){
        return  function(...$args){
            var_dump($this); // A
            var_dump(self::class); //A
            var_dump($args); // 1,2,3
            return 'ok';
        };
    }
}

class A{}
$obj = new A;
$app= new App;
$callback=$app->run();
//$cb=$callback->bindTo($obj,A::class);
//$cb=Closure::bind($callback,$obj,A::class);
//$ret=$cb();
$ret=$callback->call($obj,1,2,3);
// 与 call_user_func($cb,[1,2,3])一样
echo $ret;

fromCallable

把普通函数转成匿名函数

function foo(){
    echo 'hello';
}
$callback=Closure::fromCallable('foo');
$callback();

同过反射获取.性能差不多.

$ref = new ReflectionFunction('foo');
$callback = $ref->getClosure();

各种应用实例

调用私有方法

私有方法不能调用怎么办?可以把私有方法转成匿名函数
class C {
    private $name;
    public function __construct($name) {
        $this->name=$name;
    }
    private  function test(...$args){
        echo 'private:'.$this->name;
    }
}
$objC=new C('张三');
// $objC->test(); // Error: Call to private method
$ref = new ReflectionClass(get_class($objC));
$test = $ref->getMethod('test')
    ->getClosure($objC);
$test(); // private:张三

给对象添加方法

一个类里缺少一个函数,可以动态添加函数.
如上调用私有方法,可以把匿名函数绑定到里面.

php不支持像 $obj->getName=function(){};动态添加方法.

final class C {
    private $name;// 私有属性不能继承
    public function __construct($name) {
        $this->name=$name;
    }
//   缺少这个函数 
//    public function getName()
//    {
//        return $this->name;
//    }
}

$obj=new C("阿金");
//echo $obj->getName(); // 不支持getName怎么办?
// 把this指向C对象里面.
$getName=function(){
    return $this->name;
};
$getName=$getName->bindTo($obj,C::class);
echo $getName();

路由示例

类似laravel中的路由
添加addRoute
在回调中调用$this
class App{
    protected $routes;
    protected $responseStatus=200;
    protected $responseBody='';
    protected $responseContentType='text/html';

    public addRoute(string $path,callable $callback)
    {
        // 绑定当前类
        $this->routes[$path]=$callback->bindTo($this,__CLASS__);
    }

    public dispatch(string $path){

        if(isset($this->routes[$path] && is_callable($this->routes[$path])){
            $this->routes[$path]();
        }

        header('HTTP/1.1 ' . $this->responseStatus);
        header('Content-Type: ' . $this->responseContentType);
        header('Content-Length: ' . mb_strlen($this->responseBody));
        echo $this->responseBody;
    }
}

$app=new App();
$app->addRoute('/',function(){
    // 调用this
    $this->responseBody='home';
});

$app->addRoute('/hello',function(){
    $this->responseBody='hello';
});

$app->dispatch('/hello'); // out: hello

模板示例

php模板渲染,需要独立的作用域.
支持局部变量和this

tpl.php 模板文件如下.

<html>
<head>
    <title><?= $this->title ?></title>
    <?php css_asset('main.css') ?>
</head>
<body>
    <p>
        name:<?= $name ?>
    </p>
</body>

Template.php

class Template{
    private $data=[];

    public function setData($key,$val){
        $this->data[$key]=$val;
    }

    // 渲染
    function render($context, $tpl){
        $data = $this->data;
        $closure = function($tpl)use($data){
            extract($data);
            ob_start();
            include $tpl;
            return ob_end_flush();
        };

        $closure = $closure->bindTo($context, $context);
        $closure($tpl);
    }
}

class Post {
    private $title="文章";
}
class News {
    private $title="新闻";
}

$template=new Template;
$template->setData('name','arkin');
$template->render(new Post,'tpl.php');
echo PHP_EOL.'----'.PHP_EOL;
$template->render(new News,'tpl.php');

输出:

-w363

原作者:阿金
本文地址:https://hi-arkin.com/archives/php-lambda-closure.html

标签: php closure lambda call bind

(本篇完)

评论