为什么应该优先使用call_user_func_array而不是常规调用函数?

50
function foobar($arg, $arg2) {
    echo __FUNCTION__, " got $arg and $arg2\n";
}
foobar('one','two'); // OUTPUTS : foobar got one and two 

call_user_func_array("foobar", array("one", "two")); // // OUTPUTS : foobar got one and two 

我看到常规方法和call_user_func_array方法的输出结果相同,那么为什么应该更喜欢它呢?

在什么情况下普通调用方法会失败但call_user_func_array不会失败?

我可以得到任何这样的例子吗?

谢谢您。


14
call_user_func_array 主要用于动态情境,当你事先不知道要调用哪个函数和传递哪些参数时使用。它可以帮助你在运行时决定要调用哪个函数和传递哪些参数。 - knittl
我们不能使用常规的方法来处理参数数组吗?@elclanrs - Prince Singh
6个回答

117
  1. 你有一个数组作为函数的参数,但它的长度是不确定的。

$args = someFuncWhichReturnsTheArgs();

foobar( /* put these $args here, you do not know how many there are */ );

另一种选择是:

switch (count($args)) {
    case 1:
        foobar($args[0]);
        break;
    case 2:
        foobar($args[0], $args[1]);
        break;
    ...
}

哪个不是解决方案。

使用情景可能很少,但当你遇到它时,你需要它。


15
在PHP 5.6中,使用参数解包(argument unpacking)可以解决这个特定的问题:foobar(...$args); - DisgruntledGoat
foobar 函数的定义中参数列表是什么? - Istiaque Ahmed
@deceze,在你的回答中,你在哪里使用了 call_user_func_array - Istiaque Ahmed
@Istiaque 我假设你知道 call_user_func_array 的作用,我回答的是在什么情况下使用它以及替代方案会是什么样子。 - deceze
@deceze,为什么要使用call_user_func_array需要了解它所扮演的角色。试图理解整个不清晰的情况。 - Istiaque Ahmed
显示剩余3条评论

14
如果你不知道要传递给函数的参数个数,建议使用call_user_func_array();另一个场景是在不提前知道要调用的函数时,例如array($obj, 'method'),这也是可以使用call_user_func()的情况。与之相反,使用常规的调用方法可能会失败。
$fn = array($obj, 'method');
$args = [1, 2, 3];
call_user_func_array($fn, $args);
请注意,使用call_user_func_*函数无法调用私有或受保护的方法。
所有这一切的替代方案是让您的函数接受一个数组作为它唯一的参数:
myfn([1, 2, 3]);

然而,这样做会消除在函数声明中对每个参数进行类型提示的可能性,通常被认为是代码异味。


如果您事先不知道要传递多少参数给函数,建议使用call_user_func_array();您能否提供相关的代码示例? - Istiaque Ahmed
1
@IstiaqueAhmed,示例在下一个段落中给出,您还期望什么? - Ja͢ck
请给我们提供 $obj 变量的定义以及它的 method - Istiaque Ahmed
@IstiaqueAhmed $obj有一个名为method的函数,它需要三个参数... - Ja͢ck
你知道必须传递3个参数。那么call_user_func_array在这里扮演什么角色? - Istiaque Ahmed
显示剩余4条评论

8

您应该优先像平常一样调用函数。使用 call_user_func_array 和动态参数。例如:

function func(arg1, arg2, arg3) {
  return "$arg1, $arg2, $arg3";
}

func(1, 2, 3); //=> "1, 2, 3"

$args = range(5,7); // dynamic arguments
call_user_func_array('func', $args); //=> "5, 6, 7"

如果数组长度与函数参数数量不匹配,且函数未使用默认值,会发生什么?是错误吗?还是其他参数只是变成null(如果太少)或被忽略(如果太多)? - Mr_Moneybags
你能否更明确地解释一下“动态参数”的含义? - Istiaque Ahmed

7

call_user_func_array执行“取消柯里化”操作,这是“柯里化”的反义词。

以下内容适用于PHP的所有“可调用函数”(命名函数、闭包、方法、__invoke等),因此为简单起见,让我们忽略差异,只关注闭包。

如果我们想要接受多个参数,PHP让我们使用3种不同的API。通常的方式是:

$usual = function($a, $b, $c, $d) {
             return $a + $b + $c + $d;
         };
$result = $usual(10, 20, 30, 40);  // $result == 100

另一种方式被称为 柯里化 形式:

$curried = function($a) {
               return function($b) use ($a) {
                          return function($c) use ($a, $b) {
                                     return function($d) use ($a, $b, $c) {
                                                return $a + $b + $c + $d;
                                            };
                                 };
                      };
           };
$result = call_user_func(
              call_user_func(
                  call_user_func(
                      $curried(10),
                      20),
                  30),
              40);  // $result == 100

优点是所有柯里化函数都可以以相同的方式调用:只需给它们一个参数。
如果需要更多的参数,就会返回更多的柯里化函数,这些函数“记住”了先前的参数。这使我们可以现在传入一些参数,稍后再传入其余的参数。
这样做存在一些问题:
- 显然,用这种方式编写和调用函数非常繁琐。 - 如果我们提供柯里化函数,当它们的“记忆”能力不需要时,它们会很笨拙。 - 如果我们依赖于柯里化函数的“记忆”能力,当其他人的代码没有提供它时,我们会感到失望。
我们可以通过使用转换函数(免责声明:那是我的博客)来解决所有这些问题。这使我们可以按照通常的方式编写和调用函数,但是赋予它们与柯里化函数相同的“记忆”能力。
$curried = curry(function($a, $b, $c, $d) {
                     return $a + $b + $c + $d;
                 });
$result1 = $curried(10, 20, 30, 40);  // $result1 = 100
$result2 = call_user_func($curried(10, 20), 30, 40); // $result2 = 100

第三种方式被称为“非柯里化”,它将所有参数一次性传入:
$uncurried = function($args) {
                 return $args[0] + $args[1] + $args[2] + $args[3];
             };
$result = $uncurried([10, 20, 30, 40]);  // $result == 100

就像柯里化函数一样,非柯里化函数也可以通过一个参数进行调用,只不过这次参数是一个数组。我们仍然面临着与柯里化函数相同的兼容性问题:如果我们选择使用非柯里化函数,我们不能指望其他人也做出同样的选择。因此,我们还需要一个将非柯里化函数转换为柯里化函数的函数。这就是call_user_func_array所做的事情:

$uncurried = function($args) use ($usual) {
                 return call_user_func_array($usual, $args);
             };
$result1 = $usual(10, 20, 30, 40);  // $result1 = 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 = 100

有趣的是,我们可以通过柯里化call_user_func_array来消除多余的function($args)包装(这个过程被称为“Eta-Reduction”)。
$uncurried = curry('call_user_func_array', $usual);

$result = $uncurried([10, 20, 30, 40]); // $result == 100

不幸的是,call_user_func_array 没有 curry 那么智能;它无法自动在两者之间转换。我们可以编写自己的 uncurry 函数来实现这个功能:

function uncurry($f)
{
    return function($args) use ($f) {
               return call_user_func_array(
                          $f,
                          (count(func_get_args()) > 1)? func_get_args()
                                                      : $args);
           };
}

$uncurried = uncurry($usual);
$result1 = $uncurried(10, 20, 30, 40); // $result1 == 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 == 100

这些转换函数表明了PHP定义函数的“通常”方式实际上是多余的:如果我们用“聪明”的柯里化或非柯里化函数替换PHP的“通常”函数,很多代码仍然可以正常工作。如果这样做,最好将所有东西都柯里化,并根据需要选择非柯里化,因为这比相反的情况更容易。
不幸的是,一些使用func_get_args期望变量数量的东西以及具有默认参数值的函数会出现问题。
有趣的是,默认值只是柯里化的一种特殊形式。如果我们将那些参数放在第一个位置而不是最后,并且提供一堆备选定义来在默认情况下进行柯里化,我们大多数情况下可以不用它们。例如:
$defaults = function($a, $b, $c = 30, $d = 40) {
                return $a + $b + $c + $d;
            };
$def1 = $defaults(10, 20, 30, 40);  // $def1 == 100
$def2 = $defaults(10, 20, 30);      // $def2 == 100
$def3 = $defaults(10, 20);          // $def3 == 100

$curried = function($d, $c, $a, $b) {
               return $a + $b + $c + $d;
           };
$curriedD  = $curried(40);
$curriedDC = $curriedD(30);

$cur1 = $curried(10, 20, 30, 40);  // $cur1 == 100
$cur2 = $curriedD(10, 20, 30);     // $cur2 == 100
$cur3 = $curriedDC(10, 20);        // $cur3 == 100

5
从php 5.6开始,要向函数传递一个数组而不是参数列表,只需在数组前面加上省略号即可(这称为“参数解包”)。
function foo($var1, $var2, $var3) {
   echo $var1 + $var2 + var3;
}

$array = [1,2,3];

foo(...$array);  // 6
// same as call_user_func_array('foo',$array);

截至PHP 5.6,call_user_func_array()与变量函数的区别在于变量函数不允许您调用静态方法:
$params = [1,2,3,4,5];

function test_function() {
  echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
}    

// Normal function as callback
$callback_function = 'test_function';
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // 1+2+3+4+5=15

class TestClass
{
  static function testStaticMethod() {
    echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
  }

  public function testMethod() {
    echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
  }
}

// Class method as callback
$obj = new TestClass;
$callback_function = [$obj,'testMethod'];
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // 1+2+3+4+5=15

// Static method callback
$callback_function = 'TestClass::testStaticMethod';
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // Fatal error: undefined function

PHP 7添加了通过变量函数调用静态方法的能力,因此从PHP 7开始,this的区别不再存在。总之,call_user_func_array()使您的代码具有更大的兼容性。


0
<?php

class Demo {

    public function function1() {
        echo 'in function 1';
    }
}

$obj = new Demo();

$function_list = get_class_methods('Demo');

print_r($function_list);  //Array ( [0] => function1 )

call_user_func_array(array($obj, $function_list[0]), array()); 

// Output => in function 1

?>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接