PHP Clourse(閉包類) 淺析

一隻賤熊貓發表於2019-03-02

0x00 前言

閉包是指在建立時封裝周圍狀態的函式。即使閉包所在的環境不存在了,閉包中封裝的狀態依然存在。

在 PHP 裡所有的閉包都是 Clourse 類所例項化的一個物件,也就是說閉包與其他 PHP 物件沒有什麼不同。而一個物件就必然有其方法和屬性,這篇文章將總結 PHP 中閉包的基礎用法和 Clourse 類方法的作用。

0x01 閉包基本用法

下面看看最基本的閉包使用方法:

<?php
$hello = function ($word) {
    return `hello ` . $word;
};

echo $hello(`world`);
// 輸出 hello world
複製程式碼

嘿,這段程式碼最直觀的感受就是將一個函式賦值給了 $hello 變數,然後通過 $hello 直接呼叫它。但是這個閉包並沒有從父作用域中繼承變數(就是封裝周圍狀態),我們可以通過 use 關鍵字從閉包的父作用域繼承變數。示例如下:

<?php
$name = `panda`;

$hello = function () use ($name) {
    return `hello ` . $name;
};

echo $hello();
// 輸出 hello panda
複製程式碼

PHP 7.1 起,use 不能傳入此類變數: superglobals、 $this 或者和引數重名。

此外在使用 use 關鍵字時,父作用域的變數是通過值傳遞進閉包的。也就是說一旦閉包建立完成,外部的變數即使修改也不會影響傳遞進閉包內的值(就是即使閉包所在的環境不存在了,閉包中封裝的狀態依然存在)。示例如下:

<?php
$name = `panda`;

$hello = function () use ($name) {
    return `hello ` . $name;
};

$name = `cat`;

echo $hello();
// 輸出 hello panda
複製程式碼

傳遞變數的引用可以使閉包修改外部變數的值,示例如下:

<?php
$name = `panda`;

$changeName = function () use (&$name) {
    $name = `cat`;
};

$changeName();

echo $name;
// 輸出 cat
複製程式碼

注意:PHP 中傳遞物件時,預設是以引用傳遞所以在閉包內操作 use 傳遞的物件時需要特別注意。示例如下:

<?php
class Dog {
    public $name = `Wang Cai`;
}

$dog = new Dog();

$changeName = function () use ($dog) {
    $dog->name = `Lai Fu`;
};

$changeName();

echo $dog->name;
// 輸出 Lai Fu
複製程式碼

0x02 Clourse 類

證明閉包只是 Clourse 類物件

<?php
$clourse = function () {
    echo `hello clourse`;
};

if (is_object($clourse)) {
    echo get_class($clourse);
}
// 輸出 Closure
複製程式碼

上面的程式碼將輸出 Closure 證明了閉包只是一個普通的 Closure 類物件。

Clourse 類摘要

我們可以從 PHP 官方手冊 看到閉包類的相關資訊,下面是我在 PhpStorm 的本地文件檢視到 Clourse 類摘要。

/**
 * Class used to represent anonymous functions.
 * <p>Anonymous functions, implemented in PHP 5.3, yield objects of this type.
 * This fact used to be considered an implementation detail, but it can now be relied upon.
 * Starting with PHP 5.4, this class has methods that allow further control of the anonymous function after it has been created.
 * <p>Besides the methods listed here, this class also has an __invoke method.
 * This is for consistency with other classes that implement calling magic, as this method is not used for calling the function.
 * @link http://www.php.net/manual/en/class.closure.php
 */
final class Closure {

    /**
     * This method exists only to disallow instantiation of the Closure class.
     * Objects of this class are created in the fashion described on the anonymous functions page.
     * @link http://www.php.net/manual/en/closure.construct.php
     */
    private function __construct() { }

    /**
     * This is for consistency with other classes that implement calling magic,
     * as this method is not used for calling the function.
     * @param mixed $_ [optional]
     * @return mixed
     * @link http://www.php.net/manual/en/class.closure.php
     */
    public function __invoke(...$_) { }

    /**
     * Duplicates the closure with a new bound object and class scope
     * @link http://www.php.net/manual/en/closure.bindto.php
     * @param object $newthis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
     * @param mixed $newscope The class scope to which associate the closure is to be associated, or `static` to keep the current one.
     * If an object is given, the type of the object will be used instead.
     * This determines the visibility of protected and private methods of the bound object.
     * @return Closure Returns the newly created Closure object or FALSE on failure
     */
    function bindTo($newthis, $newscope = `static`) { }

    /**
     * This method is a static version of Closure::bindTo().
     * See the documentation of that method for more information.
     * @static
     * @link http://www.php.net/manual/en/closure.bind.php
     * @param Closure $closure The anonymous functions to bind.
     * @param object $newthis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
     * @param mixed $newscope The class scope to which associate the closure is to be associated, or `static` to keep the current one.
     * If an object is given, the type of the object will be used instead.
     * This determines the visibility of protected and private methods of the bound object.
     * @return Closure Returns the newly created Closure object or FALSE on failure
     */
    static function bind(Closure $closure, $newthis, $newscope = `static`) { }

    /**
     * Temporarily binds the closure to newthis, and calls it with any given parameters.
     * @link http://php.net/manual/en/closure.call.php
     * @param object $newThis The object to bind the closure to for the duration of the call.
     * @param mixed $parameters [optional] Zero or more parameters, which will be given as parameters to the closure.
     * @return mixed
     * @since 7.0
     */
    function call ($newThis, ...$parameters) {}
    
    /**
     * @param callable $callable
     * @return Closure
     * @since 7.1
     */
    public static function fromCallable (callable $callable) {}
}
複製程式碼

首先 Clourse 類為 final 類,也就是說它將無法被繼承,其次它的建構函式 __construct 被設為 private 即無法通過 new 關鍵字例項化閉包物件,這兩點保證了閉包只能通過 function (...) use(...) {...} 這種語法例項化 。

為什麼閉包可以當作函式執行?

從上面的類摘要中我們看出 Clourse 類實現了 __invoke 方法,在 PHP 官方手冊中對該方法解釋如下:

當嘗試以呼叫函式的方式呼叫一個物件時,__invoke() 方法會被自動呼叫。

這就是閉包可以被當作函式執行的原因。

繫結指定的$this物件和類作用域

在允許使用閉包路由的框架中(如:Slim),我們可以看見如下寫法:

$app->get(`/test`, function () {
    echo $this->request->getMethod();
});
複製程式碼

在一個閉包居然能中使用 $this?這個 $this 指向哪個物件?

通過 bindTobind 方法都能夠實現繫結 $this 和類作用域的功能,示例如下:

<?php

class Pandas {
    public $num = 1;
}

$pandas = new Pandas();

$add = function () {
    echo ++$this->num . PHP_EOL;
};

$newAdd1 = $add->bindTo($pandas);
$newAdd1();
// 輸出 2
$newAdd2 = Closure::bind($add, $pandas);
$newAdd2();
// 輸出 3
複製程式碼

上面的這段例子將指定物件繫結為閉包的 $this,但是我們並沒有指定類作用域。所以如果將 Pandas 類的 $num 屬性改寫為 protectedprivate 則會丟擲一個致命錯誤!

Fatal error: Uncaught Error: Cannot access protected property Pandas::$num

在需要訪問繫結物件的非公開屬性或方法時,我們需要指定類作用域,示例如下:

<?php

class Pandas {
    protected $num = 1;
}

$pandas = new Pandas();

$add = function () {
    echo ++$this->num . PHP_EOL;
};

$newAdd1 = $add->bindTo($pandas, $pandas);
$newAdd1();
// 輸出 2
$newAdd2 = Closure::bind($add, $pandas, `Pandas`);
$newAdd2();
// 輸出 3
複製程式碼

這裡我們看見 bindTobind 方法都指定了 $newscope 引數,$newscope 引數預設為 static 即不改變類作用域。$newscope 引數接受類名或物件,並將閉包的類作用域改為指定的類作用域,此時 Pandas 類的 $num 屬性便能夠被閉包訪問。

一次性繫結 $this 物件和類作用域並執行(PHP7)

bindTobind 方法每次指定新的物件和類作用域時都要將原閉包進行復制然後返回新的閉包,在需要多次修改繫結物件的情景下便顯得繁瑣,所以 PHP7 提供了一個新的方法 call 它能將閉包臨時的繫結到一個物件中(類作用域同時被修改為該物件所屬的類)並執行。示例如下:

<?php

class Pandas {
    protected $num = 1;
}

$pandas = new Pandas();

$add = function ($num) {
    $this->num += $num;
    echo $this->num . PHP_EOL;
};

$add->call($pandas, 5);
// 輸出 6
複製程式碼

Callable 轉為閉包(PHP7.1)

在 PHP7.1 中 Closure 類存在 fromCallable 方法能夠將 callable 型別的值轉為閉包,示例如下:

<?php

class Foo
{
    protected $num = 1;

    public static function hello(string $bar)
    {
        echo `hello ` . $bar;
    }
}

$hello = Closure::fromCallable([`Foo`, `hello`]);
$hello(`world`);
複製程式碼

這種寫法還是挺爽的畢竟通過閉包呼叫總比用 call_user_func 函式呼叫爽的多^_^。

0x03 總結

更多相關內容請看 Closure 類匿名函式,因為 PHP 官方手冊中文版的 Closure 類沒有更新,所以沒有 callfromCallable 方法的內容,推薦大家看英文版(ㄒoㄒ)。

PHP Clourse(閉包類) 淺析 – 我的部落格原文

相關文章