39

本主题扩展了我何时/应该在 PHP 中使用 __construct()、__get()、__set() 和 __call()? 其中谈到__construct,__get__set魔术方法。

从 PHP 5.3 开始,有一个新的魔术方法称为__invoke. __invoke当脚本尝试将对象作为函数调用时,将调用该方法。

现在在我对这个方法所做的研究中,人们把它比作 Java 方法.run()——参见Interface Runnable

考虑了很久,我想不出你为什么会打电话$obj(); 而不是打电话$obj->function();

即使您正在迭代对象数组,您仍然会知道要运行的主函数名称。

那么__invoke魔术方法是另一个例子,“仅仅因为你可以,并不意味着你应该”在 PHP 中的快捷方式,还是在某些情况下这实际上是正确的做法?

4

7 回答 7

49

当您需要一个必须维护某些内部状态的可调用对象时,使用 是__invoke有意义的。假设您要对以下数组进行排序:

$arr = [
    ['key' => 3, 'value' => 10, 'weight' => 100], 
    ['key' => 5, 'value' => 10, 'weight' => 50], 
    ['key' => 2, 'value' => 3, 'weight' => 0], 
    ['key' => 4, 'value' => 2, 'weight' => 400], 
    ['key' => 1, 'value' => 9, 'weight' => 150]
];

usort函数允许您使用某些函数对数组进行排序,非常简单。但是在这种情况下,我们想使用它的内部数组'value'键对数组进行排序,可以这样做:

$comparisonFn = function($a, $b) {
    return $a['value'] < $b['value'] ? -1 : ($a['value'] > $b['value'] ? 1 : 0);
};
usort($arr, $comparisonFn);

// ['key' => 'w', 'value' => 2] will be the first element, 
// ['key' => 'w', 'value' => 3] will be the second, etc

现在也许您需要再次对数组进行排序,但是这次使用'key'作为目标键,有必要重写函数:

usort($arr, function($a, $b) {
    return $a['key'] < $b['key'] ? -1 : ($a['key'] > $b['key'] ? 1 : 0);
});

如您所见,该函数的逻辑与前一个相同,但是由于需要使用不同的键进行排序,我们不能重用前一个。这个问题可以通过在__invoke方法中封装比较逻辑并定义要在其构造函数中使用的键的类来解决:

class Comparator {
    protected $key;

    public function __construct($key) {
            $this->key = $key;
    }

    public function __invoke($a, $b) {
            return $a[$this->key] < $b[$this->key] ? 
               -1 : ($a[$this->key] > $b[$this->key] ? 1 : 0);
    }
}

实现它的 Class 对象__invoke是“可调用的”,它可以在函数可能存在的任何上下文中使用,所以现在我们可以简单地实例化Comparator对象并将它们传递给usort函数:

usort($arr, new Comparator('key')); // sort by 'key'

usort($arr, new Comparator('value')); // sort by 'value'

usort($arr, new Comparator('weight')); // sort by 'weight'

以下段落反映了我的主观意见,因此,如果您愿意,现在可以停止阅读答案;):尽管前面的示例显示了 非常有趣的用法__invoke,但这种情况很少见,我会避免使用它,因为它确实可以完成令人困惑的方式,通常有更简单的实现替代方案。在同一排序问题中的一个替代示例是使用返回比较函数的函数:

function getComparisonByKeyFn($key) {
    return function($a, $b) use ($key) {
            return $a[$key] < $b[$key] ? -1 : ($a[$key] > $b[$key] ? 1 : 0);
    };
}
usort($arr, getComparisonByKeyFn('weight'));
usort($arr, getComparisonByKeyFn('key'));
usort($arr, getComparisonByKeyFn('value'));

虽然这个例子需要对lambdas有更多的了解| 关闭| 匿名函数更简洁,因为它不会创建一个完整的类结构来存储外部值。

于 2016-02-08T18:58:27.573 回答
35

这个答案在 2009 年写的有点过时了。请持保留态度。

PHP 不允许像其他语言那样传递函数指针。函数不是PHP 中的第一类。函数是第一类主要意味着你可以将一个函数保存到一个变量中,然后随时传递并执行它。

__invoke方法是 PHP 可以容纳伪一流函数的一种方式。

__invoke方法可用于传递可以充当闭包延续的类,或者仅作为可以传递的函数。

许多函数式编程依赖于一流的函数。即使是普通的命令式编程也可以从中受益。

假设您有一个排序例程,但想要支持不同的比较功能。好吧,你可以有不同的比较类来实现 __invoke 函数并将实例传递给类到你的排序函数,它甚至不必知道函数的名称。

真的,你总是可以做一些事情,比如传递一个类并让一个函数调用一个方法,但是现在你几乎可以谈论传递一个“函数”而不是传递一个类,尽管它不像其他语言那样干净。

于 2009-05-20T14:16:15.597 回答
15

我相信这个功能的存在主要是为了支持 5.3 的新闭包功能。闭包作为类的实例公开Closure,并且可以直接调用,例如$foo = $someClosure();. 一个实际的好处__invoke()是可以创建标准的回调类型,而不是使用字符串、对象和数组的奇怪组合,具体取决于您引用的是函数、实例方法还是静态方法。

于 2009-05-20T14:15:22.027 回答
5

如果你知道你正在处理某种类型的对象,你真的不应该打电话$obj();$obj->function();也就是说,除非你想让你的同事摸不着头脑。

__invoke方法适用于不同的情况。特别是当您希望提供一个通用的可调用对象作为参数时。

想象一下,你在一个类中有一个方法(你必须使用并且不能更改),它只接受一个可调用对象作为参数。

$obj->setProcessor(function ($arg) {
    // do something costly with the arguments
});

现在假设您想要缓存和重用一个冗长操作的结果,或者访问该函数之前使用的参数。定期关闭可能会很厚实。

// say what? what is it for?
$argList = [];

$obj->setProcessor(function ($arg) use (&$argList) {
    static $cache;
    // check if there is a cached result...
    // do something costly with the arguments
    // remember used arguments
    $argList[] = $arg;
    // save result to a cache
    return $cache[$arg] = $result;
});

看,如果你碰巧需要$argList从其他地方访问,或者只是清理停滞条目的缓存,你就有麻烦了!

这里来__invoke救援:

class CachableSpecificWorker
{
    private $cache = [];

    private $argList = [];

    public function __invoke($arg)
    {
        // check if there is a cached result...

        // remember used arguments
        $this->argList[] = $arg;

        // do something costly with the arguments

        // save result to a cache
        return $this->cache[$arg] = $result;
    }

    public function removeFromCache($arg)
    {
        // purge an outdated result from the cache
        unset($this->cache[$arg]);
    }

    public function countArgs()
    {
        // do the counting
        return $resultOfCounting;
    }
}

使用上面的类处理缓存数据变得轻而易举。

$worker = new CachableSpecificWorker();
// from the POV of $obj our $worker looks like a regular closure
$obj->setProcessor($worker);
// hey ho! we have a new data for this argument
$worker->removeFromCache($argWithNewData);
// pass it on somewhere else for future use
$logger->gatherStatsLater($worker);

这只是一个简单的例子来说明这个概念。可以更进一步,创建一个通用的包装器和缓存类。以及更多。

于 2016-05-12T03:01:45.000 回答
4

这是两件事的结合。您已经正确识别出其中之一。这确实就像 Java 的IRunnable接口,每个“可运行”对象都实现相同的方法。在 Java 中,该方法被命名为run; 在 PHP 中,该方法名为__invoke,您无需事先显式实现任何特定的接口类型。

第二个方面是语法糖,因此$obj->__invoke()您可以跳过方法名称而不是调用,因此看起来好像您是在直接调用对象$obj()

PHP 有闭包的关键部分是第一个。该语言需要一些已建立的方法来调用闭包对象以使其完成工作。语法糖只是让它看起来不那么难看的一种方式,就像所有带有双下划线前缀的“特殊”函数一样。

于 2009-05-20T14:24:35.893 回答
2

结论(基于以上所有)

一般来说,我认为 __invoke(){...} 魔术方法是抽象类对象主要功能的使用或直观的对象设置(在使用它的方法之前准备对象)的绝佳机会。

案例 1 - 例如,假设我使用了一些第三方对象,它实现了 __invoke 魔术方法,通过这种方式可以轻松访问对象实例的主要功能。要使用它的主要功能,我只需要知道 __invoke 方法需要什么参数以及这个函数(闭包)的最终结果是什么。通过这种方式,我能够使用类对象的主要功能,只需很少的努力来研究对象功能(请注意,在此示例中,我们不需要知道或使用任何方法名称)。

从真实代码中抽象...

代替

$obj->someFunctionNameInitTheMainFunctionality($arg1, $arg2);

我们现在使用:

$obj($arg1, $arg2);

我们现在还可以将对象传递给其他期望其参数可调用的函数,就像在常规函数中一样:

代替

someFunctionThatExpectOneCallableArgument($someData, [get_class($obj), 'someFunctionNameInitTheMainFunctionality']);

我们现在使用:

someFunctionThatExpectOneCallableArgument($someData, $obj);

__invoke 还提供了一个很好的使用快捷方式,那么为什么不使用它呢?

于 2017-05-18T20:49:26.347 回答
2

当脚本尝试将对象作为函数调用时,会调用__invoke()方法。

注意:此功能自PHP 5.3.0.

<?php
    class CallableClass
    {
        public function __invoke($x)
        {
            var_dump($x);
        }
    }
    $obj = new CallableClass;
    $obj(5);
    var_dump(is_callable($obj));
?>

上面的示例将输出:

整数(5)

布尔(真)

点击查看参考

于 2020-02-25T14:54:11.917 回答