解析PHP閉包及Clourse類方法的作用

2022-02-05 04:00:07

PHP Clourse(閉包類) 淺析

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 指向哪個物件?

通過 bindTo 和 bind 方法都能夠實現繫結 $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 屬性改寫為 protected 或 private 則會丟擲一個致命錯誤!

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

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

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

bindTo 和 bind 方法每次指定新的物件和類作用域時都要將原閉包進行復制然後返回新的閉包,在需要多次修改繫結物件的情景下便顯得繁瑣,所以 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 類沒有更新,所以沒有 call 和 fromCallable 方法的內容,推薦大家看英文版(ㄒoㄒ)。

以上就是解析PHP閉包及Clourse類方法的作用的詳細內容,更多請關注TW511.COM其它相關文章!