类型提示 - 指定对象数组

41
如何将参数类型指定为数组?
假设有一个名为“Foo”的类:
class Foo {}

我有一个接受该类类型作为参数的函数:

function getFoo(Foo $f) {}

当我传入由“Foo”组成的数组时,会出现错误,错误信息如下:

Catchable fatal error: Argument 1 passed to getFoo() must be an instance of Foo, array given

有没有办法解决这个问题?也许可以尝试以下方法:

function getFoo(Foo $f[]) {}

1
它可以是数组或者对象。PHP并不关心数组内的内容。你无法指定“我想让这个函数接受数组,但我只想让这个数组具有Foo类型的对象”。 - Royal Bg
嗯...@RoyalBg 你确定吗?那太不幸了... - Yoav Kadosh
@RoyalBg是正确的。我对此感到困惑。如果您正在传递对象数组,则只需为函数设置function getFoo($f) { ... }即可。因此,如果您想要一个专门处理数组的函数,只需创建一个新函数function getFooArray($f) { ... }即可。 - sjagr
是的,我认为Royal Bg说得对。你可以有一个function getFoos(Array $f),它解析数组并将每个$f[$i]传递给function getFoo(Foo $g) - Frank Conry
1
你需要编写一个只接受Foo对象的集合类。可以参考@bishop的答案来了解一般思路。一些PHP库有通用的集合对象,例如Doctrine的Collection对象。 - Jeremy Kendall
3
只是想知道我们对于这个使用展开运算符的解决方案的看法:function foo(Abc ...$args) { } foo(...$arr);,具体内容请参考此处 - Shahid
6个回答

44

如果您想确保正在使用“Foo数组”,并且想确保方法接收“Foo数组”,则可以:

class ArrayOfFoo extends \ArrayObject {
    public function offsetSet($key, $val) {
        if ($val instanceof Foo) {
            return parent::offsetSet($key, $val);
        }
        throw new \InvalidArgumentException('Value must be a Foo');
    }
}

那么:

function workWithFoo(ArrayOfFoo $foos) {
    foreach ($foos as $foo) {
        // etc.
    }
}

$foos = new ArrayOfFoos();
$foos[] = new Foo();
workWithFoo($foos);

秘密武器在于你定义了一个新的"foo数组类型",然后使用类型提示保护传递该"类型"。


如果你不想自己制作,Haldayne库会处理成员资格要求检查的样板文件:

class ArrayOfFoo extends \Haldayne\Boost\MapOfObjects {
    protected function allowed($value) { return $value instanceof Foo; }
}

(披露全文,我是Haldayne的作者。)


历史注释:2014年,Array Of RFC提出了这个特性。RFC以4票支持和16反对被拒绝。最近这个概念在内部列表上重新出现,但投诉与最初的RFC相同:添加此检查将显着影响性能


好的解决方案,但是您忘记重写构造函数并在那里添加检查了。 - Alexander
你不能在这里返回一个空函数值,应该返回 parent::offsetSet($key, $val);。 - ali

23

虽然这是一个旧的帖子,但可变函数和数组展开可以(在一定程度上)用于实现类型化数组提示,至少在PHP7中可以实现(我没有测试早期版本)。

示例:

class Foo {
  public function test(){
    echo "foo";
  }   
};  

class Bar extends Foo {
  //override parent method
  public function test(){
    echo "bar";
  }   
}          

function test(Foo ...$params){
  foreach($params as $param){
    $param->test();
  }   
}   

$f = new Foo();
$b = new Bar();

$arrayOfFoo = [$f,$b];

test(...$arrayOfFoo);
//will output "foobar"


局限性:

  1. 从技术上讲,这不是一个解决方案,因为你实际上没有传递一个类型化数组。相反,你使用数组展开运算符1(在函数调用中的“...”)将数组转换为参数列表,每个参数都必须是变参声明2中指定的类型(它还使用省略号)。

  2. 函数调用中的“...”是绝对必要的(鉴于上述情况,这并不令人惊讶)。试图调用

test($arrayOfFoo)
在上述示例的情况下,使用数组作为参数将导致类型错误,因为编译器期望foo的参数,而不是数组。请参见下面的内容,了解传递给定类型的数组的一种方法,虽然有点巧妙,但仍保留了某些类型提示。
可变函数只能有一个可变参数,并且它必须是最后一个参数(否则编译器如何确定可变参数何时结束并开始下一个参数),这意味着您不能声明类似以下方式的函数:
function test(Foo ...$foos, Bar ...$bars){ 
    //...
}
或者
function test(Foo ...$foos, Bar $bar){
    //...
}


仅稍微好于逐个检查元素的替代方案:

以下步骤实际上比仅检查每个元素类型要好,因为它保证了函数体中使用的参数具有正确的类型,而不会使函数充满类型检查,并且它会抛出通常的类型异常。

考虑:

function alt(Array $foos){
    return (function(Foo ...$fooParams){

        //treat as regular function body

        foreach($fooParams as $foo){
            $foo->test();
        }

    })(...$foos);
}

这个想法是定义并返回一个立即调用的闭包,该闭包会为您处理所有可变参数和解包操作。(可以进一步扩展原则,定义一个生成此结构函数的高阶函数,以减少样板代码)。在上面的示例中:

alt($arrayOfFoo) // also outputs "foobar"

这种方法存在以下问题:

(1) 对于经验不足的开发人员,可能不太清楚。

(2) 它可能会产生一些性能开销。

(3) 与仅内部检查数组元素类似,它把类型检查视为实现细节,因此必须检查函数声明(或享受类型异常)才能意识到只有特定类型的数组才是有效的参数。在接口或抽象函数中,无法编码完整的类型提示;所有人能做的就是注释说期望上述类型的实现(或类似的实现)。


注释

[1]. 简而言之:数组解包渲染等效

example_function($a,$b,$c);

example_function(...[$a,$b,$c]);


[2]. 简而言之:形如

可变参数函数。
function example_function(Foo ...$bar){
    //...
}

可以通过以下任何一种方式有效地调用:

example_function();
example_function(new Foo());
example_function(new Foo(), new Foo());
example_function(new Foo(), new Foo(), new Foo());
//and so on

9
抱歉,PHP不是这样工作的。它被设计成快速且易于编程的语言,因此您不需要担心严格的类型,但这也意味着你没有任何帮助(如类型推断编译器),很容易陷入动态类型的困境。PHP解释器对您放入数组中的内容一无所知,所以如果要验证类似以下内容的东西(当然在PHP中不起作用),它必须迭代所有数组条目:
function bar(Foo[] $myFoos)

这对性能有很大影响,当数组变得很大时会更加明显。我认为这就是PHP没有提供类型化数组提示的原因。
其他答案建议创建强类型化的数组封装器。当你使用像Java或C#这样的具有通用类型的编译器时,封装器是可以接受的,但对于PHP来说,我不同意。在这里,这些封装器是乏味的样板代码,每个类型化数组都需要创建一个。如果你想要使用库中的数组函数,你需要将你的封装器扩展到类型检查委托函数,并使你的代码膨胀。这在学术样本中可以完成,但在生产系统中,有许多类和集合,开发人员时间成本高,Web集群的MIPS也很稀缺?我认为不行。
因此,仅仅为了在函数签名中拥有PHP类型验证,我会避免使用强类型化的数组封装器。我不认为它们给你足够的回报率。好的PHP IDE的PHPDoc支持会更有帮助,而在PHPDoc标记中,Foo[]表示法可行。
另一方面,如果你能将一些好的业务逻辑集中到封装器中,那么封装器是有意义的。
也许有一天PHP会扩展强类型化的数组(或更精确地说:强类型化的字典)。我会喜欢那样。有了这些,它们可以提供不惩罚你的签名提示。

代码有误!读者可能会将其视为解决方案,这会造成困惑。 - Sliq
2
好的,我为此添加了一条注释。 - Rolf

4
class Foo {
    /**
     * @param Foo[] $f
     */
    function getFoo($f) {

    }
}

4
澄清一下,phpdoc 类型提示 @param Foo[] ... 不是 JS 的标准。然而,它非常有用;像 phpstorm 这样的 IDE 使用它来改善它们的智能感知。 - ToolmakerSteve

3
function getFoo()

通常情况下,您需要一个添加方法,该方法将类型提示为Foo
function addFoo( Foo $f )

因此,getter将返回Foo的数组,add方法可以确保您只有Foo添加到数组中。
编辑:从getter中删除参数。我不知道我在想什么,你不需要在getter中使用参数。
编辑:只是为了显示完整的类示例:
class FooBar
{
    /**
     * @return array
     */
    private $foo;

    public function getFoo()
    {
        return $foo;
    }

    public function setFoo( array $f )
    {
        $this->foo = $f;

        return $this;
    }

    public function addFoo( Foo $f )
    {
        $this->foo[] = $f;

        return $this;
    }
}

通常来说,你可能不需要setter方法,因为你已经有了add方法来确保$foo是一个由Foo组成的数组,但它有助于说明类中正在发生的事情。


0
您可以使用数据类型描述@var,如下所示:
<?php

class ClassA {

    private $message;

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

    function getMessage() {
        return "Hello {$this->message}!";
    }

}

class ClassB {

    /**
     * @var ClassA[]
    */
    private $classAList;

    function __construct() {
        $this->classAList = [];
    }
    
    function attach(ClassA $classA) {
        array_push($this->classAList, $classA);
    }

    function showMessages() {
        foreach($this->classAList as $classA) {
            // the intellisense will be capable to autocomplete this code.
            echo "{$classA->getMessage()}\n";
        }
    }

}

$classB = new ClassB();
$classB->attach(new ClassA("Person 1"));
$classB->attach(new ClassA("Person 2"));
$classB->showMessages();

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