PHP严格模式错误:通过包含文件引起

6

如果类在一个文件中,看起来严格的错误不会发生,如下所示:

abstract class Food{}

class Meat extends Food{}                    

abstract class Animal{          
    function feed(Food $food){ }
}

class Cat extends Animal{           
    function feed(Meat $meat){
        parent::feed($meat);
    }   
}    

但是如果你将类的定义放在不同的文件中,并像这样包含它们:
abstract class Food{}

class Meat extends Food{}                    

require 'Animal.php';
require 'Cat.php';

抛出严格标准错误信息:

严格标准:应该与 Animal::feed(Food $food) 兼容的声明 Cat::feed() 在 c:\path\to\Cat.php 的第...行。

如果所有内容都在一个文件中,那么这也是可以的:

class Dog extends Animal{           
  function feed($qty = 1){ 
    for($i = 0; $i < $qty; $i++){
        $Meat = new Meat();
        parent::feed($Meat);
    }       
  } 
}

这是否是预期行为?

因为肉类是一种食品,所以首先不应该有抱怨,对吧? 因此,解决方案很明确:将所有内容放在一个文件中,严格遵守标准 ;)

非常感谢任何提示。


你看到的完整信息是什么? - Jonnix
显示错误消息以及代码的包含方式 - John Conde
@Siguza 感谢您的解释,但是$Meat是一种食物,所以喂肉不就是喂食物吗? - mife
当然,Meat是一种Food,但并不是每种Food都是Meat。一个函数可以将Animal作为参数,并将其喂给PotatoPotato是一种Food,所以这是合法的,对吧?现在,由于Cat是一种Animal,因此您可以将其传递给该函数。这也是合法的,对吧?只有现在出现了这种情况,即Cat被喂了一颗Potato,而根据其类定义,这是不允许的。我会说,在这种情况下,Cat未能扩展Animal,因为它实际上缩小了其功能范围,而不是扩展(添加到)它。 - Siguza
@mife 这与PHP没有什么关系,更多的是与OOP的概念有关。我无法比下面Ja͢ck的回答更好地表达:通过使CatAnimal不兼容,您正在违反Liskov替换原则。或者通过声明任何Animal都可以被喂任何Food,这显然不是事实。但是,有时候OOP可能并不那么有趣。 - Siguza
显示剩余3条评论
2个回答

3

这是预期的行为吗?

不幸的是,是的。与类声明相关的复杂性使得在同一脚本中出现时,并不总是应用严格的规则;每个文件只有一个类就不会出现这个问题。

因为肉是食物,所以不应该有抱怨,对吧?

有两个原因是错的:

  1. 肉是比食物更小的类型,因此仅允许在您的派生类中使用更小的类型会违反LSP; 您不能用Cat替换Animal

  2. 在 PHP 中,重载方法时参数类型是不变的invariant,即接受的类型必须完全匹配父级的类型。虽然可以说逆变类型是有道理的,但由于技术原因,这是无法做到的。

所以解决方案很明显:将所有内容放在一个文件中,严格遵守标准即可满足要求;)
不,你绝不能依赖那种行为。

肉扩展了食物。它不是“较小”的类型。未违反Liskov替换原则。所有Food可以做的事情,Meat也可以做,特别是在PHP中,通过继承关闭行为是不允许的(基类中的public方法不能在子类中重新声明为privateprotected)。 - axiac
@axiac 假设 Wheat 扩展了 Food ... Cat::feed() 也应该接受这种类型,但现在它不接受。 - Ja͢ck
嗯...我想你是对的。AnimalCat之间违反了LSP原则。我之前只考虑了FoodMeat之间的情况。“如果S是T的子类型,则程序中类型为T的对象可以替换为类型为S的对象,而不会改变该程序的任何期望属性” -- 如果Cat::feed()仅接受Meat,那么Cat 无法 替换 $a = new Animal(); $a->feed(new Wheat()); 中的 Animal。我将“更小”解释为“具有较少的属性”,但实际上它是“不太通用”。 - axiac
@axiac 是的,每次我提起这个问题都要考虑一下;-) - Ja͢ck

1

这是PHP在类和命名空间方面的许多奇怪行为之一。

标准解决方案是创建一个接口(我们称其为FoodInterface),并在基类中实现它。然后,在方法feed()的参数类型中使用FoodInterface

interface FoodInterface {}

abstract class Food implements FoodInterface {}

class Meat extends Food {}

abstract class Animal {
    function feed(FoodInterface $food) {}
}

class Cat extends Animal {           
    function feed(FoodInterface $meat) {
        parent::feed($meat);
    }
}
FoodInterface接口可以为空,或者您可以在其中声明需要在Animal::feed()中调用的函数。

这样,您就可以使用实现了FoodInterface接口的任何对象来feed()您的Cat(或任何其他Animal),无论它们是否扩展了Food类。只要它们实现了接口,它们就可以被喂给任何Animal

class Toy implements FoodInterface {}

$cat = new Cat();
$cat->feed(new Toy());    // He can't eat it but at least he will have some fun :-)

因为您的基类是抽象的,它可以作为上述接口。忘记接口,只需声明与Animal::feed()具有相同参数类型的Cat::feed()。
然后,在Cat::feed()的实现中,您可以使用instanceof检查接收到的参数类型是否是您想要的类型(Meat):
abstract class Food {}

class Meat extends Food {}

abstract class Animal {
    function feed(Food $food) {}
}

class Cat extends Animal {           
    function feed(Food $meat) {
        if (! $meat instanceof Meat) {
            throw new InvalidArgumentException("Cats don't eat any Food. Meat is required");
        }

        // Here you are sure the type of $meat is Meat
        // and you can safely invoke any method of class Meat
        parent::feed($meat);
    }
}

评论

第一种方法是正确的做法。第二种方法有其自身的优点,我建议仅在第一种方法由于某些原因不可行时使用它。


在PHP中,我的理解是_接口_被视为抽象类。它们被假定具有“空方法”。 - Ryan Vincent
PHP 如何在内部处理接口和抽象类并不重要。接口和抽象类之间的重要区别有两个:1)抽象类可以实现某些或所有方法,可以拥有静态方法、类属性、静态属性和类常量(接口不能);2)一个类可以实现多个接口,但只能继承一个类(无论是抽象还是非抽象)。除了领域建模的原因外,这些是在确定某种情况下选择使用接口还是抽象类的技术原因。 - axiac

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