PHP - 对象实例化上下文 - 奇怪的行为 - 这是PHP的bug吗?

6

我不是在问为什么某段代码失败了,而是在问为什么它能够正常工作。这段代码在我的编码过程中一直运行良好,但我却希望它能够失败。

案例

  • a base abstract class with a protected constructor declared abstract
  • a parent class extends the abstract class with public constructor (Over ridding)
  • a child class extends the very same abstract class with a protected constructor

      abstract class BaseClass {
        abstract protected function __construct();
      }
    
      class ChildClass extends BaseClass {
        protected function __construct(){
          echo 'It works';
         }
      }
    
      class ParentClass extends BaseClass {
        public function __construct() {
          new ChildClass();
        }
      }
    
      // $obj = new ChildClass(); // Will result in fatal error. Expected!
    
      $obj = new ParentClass(); // that works!!WHY?
    

问题

父类实例化子类对象,居然能够成功!为什么会这样呢? 据我所知,如果一个对象的构造函数被声明为protected,除非只在内部或从任何子类中通过继承来实例化,否则无法实例化。

父类不是子类的子类,它没有从子类那里继承任何东西(但两个类都扩展了同一个基础抽象类),那么为什么实例化不会失败呢?

编辑

只有在具有抽象构造函数的抽象BaseClass情况下才会出现这种情况。如果BaseClass是具体的,或者它的受保护构造函数不是抽象的,则实例化会按预期失败...这是PHP的一个错误吗? 为了我的理智,我真的需要解释一下为什么PHP在这种非常特殊的情况下表现如此。

提前致谢


2
我对基类、父类和子类的关系有点迷糊,不知道哪个继承哪个。能否提供一些代码示例? - Phil
当然,我会在几分钟内发布一个代码示例。 - Ahmad Farouk
1
我的猜测是:BaseClass 被假定为构造函数被定义的地方(它确实是),因此根据继承规则,ParentClass 有权访问它。这是个错误吗?有争议。罕见的边缘情况?当然。 - deceze
你能否尝试将构造函数设置为私有的,而不是受保护的?这样做会很有趣,我们可以看看会发生什么变化。 - Anthony
不太确定还有什么需要补充的,特别是因为这只是我对 PHP 的“想法”的猜测。:) BaseClass::__construct 被定义为 protectedChildClass::__construct 继承了它而没有改变它,ParentClass::__constructBaseClass 继承,因此 ParentClass 可以访问 ChildClass::__construct - deceze
显示剩余8条评论
4个回答

3
注意:以下内容已在PHP 5.3.8中测试。其他版本可能表现不同。
由于PHP没有正式规范,因此无法从“应该”发生的角度回答这个问题。我们能做到的最接近的是PHP手册关于protected的声明:
“声明为protected的成员只能在类本身和继承和父类中访问。”
虽然成员可以在ChildClass中被覆盖(保持“protected”标识符),但它最初是在BaseClass中声明的,因此它仍然可在BaseClass的后代中看到。
与此解释直接相反的是,比较一下受保护属性的行为:
<?php
abstract class BaseClass {
    protected $_foo = 'foo';
    abstract protected function __construct();
}

class MommasBoy extends BaseClass {
    protected $_foo = 'foobar';
    protected function __construct(){
        echo __METHOD__, "\n";
    }
}

class LatchkeyKid extends BaseClass {
    public function __construct() {
        echo 'In ', __CLASS__, ":\n";
        $kid = new MommasBoy();
        echo $kid->_foo, "\n";
    }
}

$obj = new LatchkeyKid();

输出:

在LatchkeyKid中:
MommasBoy::__construct
致命错误:无法访问MommasBoy::$_foo的受保护属性,位于-行18

将抽象的__construct更改为具体函数并使用空实现可获得所需的行为。

abstract class BaseClass {
   protected function __construct() {}
}

然而,非魔术方法在相关类中是可见的,无论它们是否是抽象的(大多数魔术方法必须是公共的)。

<?php
abstract class BaseClass {
    abstract protected function abstract_protected();
    protected function concrete() {}
}

class MommasBoy extends BaseClass {
    /* accessible in relatives */
    protected function abstract_protected() {
        return __METHOD__;
    }
    protected function concrete() {
        return __METHOD__;
    }
}

class LatchkeyKid extends BaseClass {
    function abstract_protected() {}
    public function __construct() {
        echo 'In ', __CLASS__, ":\n";
        $kid = new MommasBoy();
        echo $kid->abstract_protected(), "\n", $kid->concrete(), "\n";
    }
}

$obj = new LatchkeyKid();

输出:

在LatchkeyKid中:
MommasBoy::abstract_protected
MommasBoy::concrete

如果您忽略警告并将魔术方法(除了__construct__destruct__clone)声明为protected,则它们似乎可以像非魔术方法一样在相关类中访问。

__clone__destruct被保护时,在相关类中也无法访问,无论它们是否是抽象的。这使我相信抽象的__construct行为是一个错误。

<?php
abstract class BaseClass {
    abstract protected function __clone();
}

class MommasBoy extends BaseClass {
    protected function __clone() {
        echo __METHOD__, "\n";
    }
}

class LatchkeyKid extends BaseClass {
    public function __construct() {
        echo 'In ', __CLASS__, ": \n";
        $kid = new MommasBoy();
        $kid = clone $kid;
    }
    public function __clone() {}
}

$obj = new LatchkeyKid();

输出:

在LatchkeyKid中:
致命错误:从上下文'LatchkeyKid'调用受保护的MommasBoy::__clone(),位于 - 的第16行

访问__clonezend_vm_def.h(具体来说是ZEND_CLONE操作码处理程序)中受到强制执行。这除了对方法的访问检查之外,可能就是它具有不同行为的原因。然而,我没有看到访问__destruct的特殊处理,所以显然还有更多内容。

PHP开发人员之一Stas Malyshev(嗨,Stas!)研究了__construct__clone__destruct,并发表了以下观点:

一般来说,定义在基类中的函数应该对该类的所有[后代]可访问。其背后的原理是,如果你在基类中定义了一个函数(即使是抽象的),那么你就表示它将对该类的任何实例(包括扩展的实例)都可用。因此,该类的任何后代都可以使用它。 [...] 我检查了为什么构造函数的行为不同,这是因为父级构造函数只有在被声明为抽象或从接口引入时才被认为是子级构造函数的原型(具有签名强制执行等)。因此,通过将构造函数声明为抽象或将其作为接口的一部分,您使其成为合同的一部分,从而可供所有层次结构访问。如果您不这样做,则构造函数彼此完全不相关(这与所有其他非静态方法不同),因此具有父构造函数并不意味着任何关于子构造函数的内容,因此父构造函数的可见性不会传递。因此,对于构造函数而言,这不是一个错误。[注意:这与J. Bruni的答案类似。] 我仍然认为__clone和__destruct最可能存在错误。 [...] 我已经提交 bug # 61782以跟踪__clone和__destruct的问题。

受保护的属性被拒绝访问,而同时受保护的构造函数却不是!根据 PHP 规范,ChildClass 对象首先不应该被实例化! - Ahmad Farouk
@Ahmad,可以这样理解:abstract 声明将方法的定义实现代码分开。如果你曾经接触过类似于 C 的头文件,你就知道它们之间的区别了。函数定义定义了可见性。属性不能被声明为 abstract,因此这种比较不起作用。 - deceze
@outis.. 感谢您的解释,但我仍然不明白为什么PHP会这样工作:)..只有在具有抽象构造函数的抽象BaseClass中才会发生这种情况。其他情况下实例化会失败。我仍然需要知道为什么会发生这种情况,但感谢您的努力。 - Ahmad Farouk

3
为什么它能工作?
因为在ParentClass内部,您已授予对来自BaseClass的抽象方法的访问权限。尽管其实现是在ChildClass中定义的,但正是这个非常相同的抽象方法被调用。
一切都取决于具体方法和抽象方法之间的区别。
您可以这样想:抽象方法是具有多个实现的单个方法。另一方面,每个具体方法都是唯一的方法。当它与其父级具有相同的名称时,它会覆盖父级的方法(而不是实现它)。
因此,当声明为abstract时,总是调用基类方法。
考虑一下声明为abstract的方法:为什么不同实现的签名不能不同?为什么子类不能以更少的可见性声明该方法?
无论如何,您刚刚发现了一个非常有趣的特性。或者,如果我上面的理解不正确,并且您期望的行为是真正期望的行为,那么您已经发现了一个错误。

你是说,如果不考虑任何 SubClass 的构造函数签名,只要它们都继承了同一个抽象的 BaseClass 并且其构造函数被声明为 abstract,那么不相关的类就可以实例化彼此? - Ahmad Farouk
这些类怎么会“无关”,它们都实现了同一个抽象类!此外,构造函数签名也是相同的(因为它们实现了相同的抽象方法)。总之,我认为这个答案实际上解释了这种行为,所以你肯定可以接受它。 - Snifff
@Snifff:可能他的意思是“不相关”而不是“无关”。无论如何,你的评论仍然适用。 - J. Bruni
@AhmadFarouk:是的,任何子类中的签名都应该被忽略 - 它必须与抽象类中声明的相同。它可能更加可见。你认为“可见性胜出”,而似乎“具体性胜出”。需要考虑的主要问题是覆盖(重载)与实现之间的区别。 - J. Bruni
1
无论如何,考虑到用户“outis”对PHP源代码的最新研究发现,我更倾向于将其视为“错误”。实际上,由于我们对两种行为都有很好的解释,因此这取决于开发人员的决定。对你来说,其中一种显然是“正确”的和“预期”的,而另一种则是“错误”的。对我来说,两者都是合理的。对我来说,他们可以改变行为以匹配您的期望,这也是可以的。结论:这更多是一种“选择”,而不是一个错误或不是(当然也不能排除可能是一个错误)。 - J. Bruni

2

编辑:构造函数的行为不同...即使没有抽象类,也应该能够正常工作,但我发现这个测试测试了相同的情况,看起来它是一种技术限制,下面解释的东西现在无法与构造函数一起使用。

这里没有bug。您需要理解访问属性是与对象上下文一起工作的。当您扩展一个类时,您的类将能够在BaseClass上下文中查看方法。ChildClass和ParentClass都在BaseClass上下文中,因此它们可以看到所有BaseClass方法。为什么需要它?为了多态性:

  class BaseClass {
     protected function a(){}
  }

  class ChildClass extends BaseClass {
    protected function a(){
      echo 'It works';
     }
  }

  class ParentClass extends BaseClass {
    public function b(BaseClass $a) {
      $a->a();
    }
    public function a() {

    }
  }

无论您将哪个子类传递给ParentClass :: b()方法,您都可以访问BaseClass方法(包括protected,因为ParentClass是BaseClass的子类,子类可以看到其父类的protected方法)。相同的行为适用于构造函数和抽象类。

不是这样的,根据我的例子,如果BaseClass是具体的而不是抽象的,对象实例化将失败...根据您的解释,两种方式都应该成功。如果BaseClass构造函数不是抽象的,则也会失败...因此,这导致了一个非常特定的情况,即BaseClass是抽象的,并且其构造函数也被声明为抽象的。 - Ahmad Farouk
@AhmadFarouk 你说得对。但是它应该可以在没有抽象类的情况下工作,但似乎存在构造函数上下文的已知问题。我更新了我的答案。 - meze
根据我的理解,实例化应该无论如何都会失败,因为ParentClass没有从ChildClass继承任何东西。除了我们现在讨论的情况之外,所有可能的情况都会发生预期的失败。 - Ahmad Farouk
@AhmadFarouk 我认为有两个方面:1)您不希望允许从另一个“非直接子类”实例化具有受保护构造函数的类 2)另一方面,您希望所有方法保持一致并以相同的方式运作。无论如何,抽象方法与否都不应更改方法的可见性 - 提交错误报告。 - meze
1
@meze.. 你现在明白我的意思了。无论是否为抽象方法,都不应该修改方法的访问权限(可见性)。因此,受保护的访问修饰符是允许或拒绝访问的主要规则,而不是抽象声明。 - Ahmad Farouk

1

我在想抽象实现底层是否存在一些错误,或者我们忽略了一些微妙的问题。将BaseClass从抽象更改为具体会产生你所期望的致命错误(类已重命名以保持清晰)。

编辑:我同意@deceze在评论中所说的,这是一个抽象实现的边缘情况,可能是一个bug。至少这是一个提供预期行为的解决方法,尽管有些丑陋的技巧(伪抽象基类)。

class BaseClass
{
    protected function __construct()
    {
        die('Psuedo Abstract function; override in sub-class!');
    }
}

class ChildClassComposed extends BaseClass
{
    protected function __construct()
    {
        echo 'It works';
    }
}


// Child of BaseClass, Composes ChildClassComposed
class ChildClassComposer extends BaseClass
{
    public function __construct()
    {
        new ChildClassComposed();
    }
}

PHP致命错误:在/Users/quickshiftin/junk-php/change-private-of-another-class.php的第46行,从'ChildClassComposer'上下文中调用受保护的ChildClassComposed::__construct()。

1
这就是我需要发生的致命错误。如果声明为抽象基类,为什么它不会在基类中发生呢?我真的需要解释一下为什么PHP在处理抽象基类时与具体类不同。 - Ahmad Farouk
看看其他人说了什么,包括其他答案。这是在抽象上下文中使用受保护构造函数的边缘情况。确保您接受其中一个答案,并使您的评分朝着正确的方向发展:D - quickshiftin
“期望行为”究竟是什么?目前的行为同样可以被认为是可预期的。 :) - deceze
@deceze 只是想让 ChildClassComposed 的构造函数在从 ChildClassComposer 中调用时引发致命错误,虽然我同意; 如果你知道你在做什么,那么当前行为是可以预期的,哈哈。只是想帮助 OP 实现他所需求的行为 :) - quickshiftin
我不是在寻求解决方法...我知道很多解决方法...我只需要知道为什么抽象类会以那种方式工作。为了保持理智,我需要解释,否则这将成为 PHP 的一个 bug。 - Ahmad Farouk
请在internals@lists.php.net上向@AhmadFarouk提问,以确定是否存在错误。 - quickshiftin

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