访问控制与继承

[TOC]

本文验证分析得到的结论

  • 类内调用,不受访问控制约束限制。类间调用,则受访问控制约束限制。
  • 只有 public 修饰的成员,才能在类外被访问。即实例化对象只可以直接访问被 public 修饰的成员。(注意:当类中声明魔术方法 __get (), 实例化对象访问类未定义或私有的成员是间接访问。)
  • 子类与父类中同时定义一个同名的方法。

    • 父类中的同名方法被 private 修饰,子类中的同名方法与父类的同名方法,不构成重写(overriding)关系。并且①如果父类中有其他公有方法调用此同名方法②子类不重写调用此同名方法的公有方法,那么子类的实例化对象访问这个公有方法,公有方法里面调用的还是父类的同名方法。(其他文章参考:从父类继承的函数中的$ this仍然指向php中的父对象?)
    • 父类中的同名方法被 public 或 protected 修饰,子类中的同名方法与父类的同名方法,构成重写(overriding)关系,但要求修饰子类的同名方法的访问修饰符的作用范围 >= 修饰子类的同名方法的访问修饰符的作用范围,否则报致命错误。一般代码编辑器也会提示。
  • $this 是一个伪变量,表示对主叫对象的引用。当一个方法在类定义内部被调用时,可使用 $this。
  • __CLASS__ 魔术常量,表示当前类。作用与 get_class() 相同,要想获取主叫对象的所属类的类名,可使用 get_called_class() 函数, 或者 get_class($this) 。

访问控制

类中的成员(属性或类常量或方法)前无任何访问修饰符修饰时,默认被 pubic 修饰。
访问控制修饰符作用范围是否参与继承
public当前类内部、子类内部、当前类外部、子类外部YES
protected当前类内部、子类内部YES
private只能在当前类内部被访问NO
注意:
只有参与继承(被 public 或 protected 修饰)的成员方法,才可以 overriding(重写)。因为 private 修饰的私有方法对于子类来说,是不可见的,即***几乎可以认为私有方法不参与继承***。此外,php 中的 (overload)重载,并不是真正意义上的、像其他语言(如java)所描述的那种概念。 PHP 重载

示例代码

下面的代码写在名为 base.php 的文件中,运行环境 PHP 8。

<?php
class Super
{
    public $name; //姓名
    public $gender; //性别
    protected $birth; //出生日期
    private $telephone; //手机号码

    // 构造方法
    public function __construct($name, $gender, $birth, $telephone)
    {
        $this->name = $name;
        $this->gender = $gender;
        $this->birth = $birth;
        $this->telephone = $telephone;
    }

    // 私有方法
    private function printHello() {
        echo __CLASS__ . ' hello' . PHP_EOL;
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        $this->printHello();
    }
}

class child extends Super
{
    // 声明一个公有的与父类中私有方法 printHello 同名的方法 printHello,看是否构成对的父类中的同名方法的重写
    private function printHello() {
        echo "阿凡提de小毛驴";
    }
}

$init = ['绘梨衣', '女', '2003-08-12', '158xxxx0812'];
$super = new Super(...$init);
$child = new child(...$init);
echo $super->printTest();
echo $child->printTest();

运行上面的代码,结果输出:

[Running] php "base.php"
Super hello
Super hello

[Done] exited with code=0 in 0.086 seconds

重点分析

将上面的代码精简一下, 重点分析子类继承父类的公有或受保护的方法中,调用父类的私有方法,而后又在子类中声明同名的私有方法, $this 引用的疑问

第一种情况:子类直接继承父类,子类内部不实现任何方法。
<?php

class Super
{
    // 私有方法 printHello
    private function printHello() {
        echo get_called_class() . ' hello' . PHP_EOL;
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        var_dump(get_class($this));
        var_dump(get_class_methods($this));
        $this->printHello();
    }
}

class child extends Super
{ 
    
}

$super = new Super();
echo $super->printTest();
echo '------------------------------------'.PHP_EOL;
$child = new child();
echo $child->printTest();

输出的结果如下。从输出结果来看,我们知道子类继承了父类的全部方法,包括 Super 私有的方法 printHello()。显然这并不符合我们的预期,因为在其他强类型面向对象语言中,private 修饰的方法被描述为不可继承的。这里我们先可以认为这是因为 php 底层实现导致的原因,先不予考虑,也几乎认为在 php 中,私有方法不能被继承。对这里有疑问的同学可先参考这篇文章,PHP内核探索:继承,多态与抽象类

基于这样的考虑,我认为子类中只存在一个公有方法 printTest(),在类外,Child 的实例化的对象 $child 在调用 printTest() 时,会由于 Child 中没有可访问的 printHello() 方法,发出 Fatal Error。然而,和我预料的不一致。$child->printTest() 时,printTest() 内部的 $this->printHello() 访问的是父类的私有方法 printHello(),且此时的 $this 是 $child 对象的引用。

这不禁让人疑惑,子类的实例化对象的引用 $this 怎么可以访问父类的私有方法?这不科学。

string(5) "Super"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
Super hello
------------------------------------
string(5) "child"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
child hello
第二种情况:子类声明一个与父类私有方法同名的 printHello() 方法

注意:此方法并不与父类中的 printHello() 构成重写关系。

<?php

class Super
{
    // 私有方法 printHello
    private function printHello() {
        echo get_called_class() . ' hello' . PHP_EOL;
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        var_dump(get_class($this));
        var_dump(get_class_methods($this));
        $this->printHello();
    }
}

class child extends Super
{ 
    public function printHello() {
        echo "阿凡提de小毛驴";
    }
}

$super = new Super();
echo $super->printTest();
echo '------------------------------------'.PHP_EOL;
$child = new child();
echo $child->printTest();

结果输出如下。从结果可以看出,$child->printTest() 时,此时的 $this 是 $child 对象的引用,printTest() 内部的 $this->printHello() 访问的依然是父类的私有方法 printHello(),而非子类中声明的 printHello()。这又让人更加疑惑了。

string(5) "Super"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
Super hello
------------------------------------
string(5) "child"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
child hello
第三种情况:子类重写父类公有方法 printTest() ,但函数体不变。
<?php

class Super
{
    // 私有方法 printHello
    private function printHello() {
        echo get_called_class() . ' hello' . PHP_EOL;
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        var_dump(get_class($this));
        var_dump(get_class_methods($this));
        $this->printHello();
    }
}

class child extends Super
{ 
    public function printTest() {
        $this->printHello();
    }
}

结果输出如下。此时的结果是符合预期的,$child->printTest() 是发生致命错误。因为 Child 中并没有可以访问的 printHello() 方法。但即使这样,对比第一种情况,也出现了新的疑问?子类中重写的 printTest() 与 继承的 printTest() 实现上是一样的,为什么第一种代码不报错,而当前的代码会报错,是因为继承的 printTest() 和 重写的 printTest() 在内存中存储的位置不一样吗?

string(5) "Super"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
Super hello
------------------------------------
Fatal error: Uncaught Error: Call to private method Super::printHello() from context 'child' on line 26
Error: Call to private method Super::printHello() from context 'child'
第四种情况:子类声明一个与父类私有方法同名的 printHello() 方法,并且子类重写父类公有方法 printTest() ,但函数体不变。
<?php

class Super
{
    // 私有方法 printHello
    private function printHello() {
        echo get_called_class() . ' hello' . PHP_EOL;
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        var_dump(get_class($this));
        var_dump(get_class_methods($this));
        $this->printHello();
    }
}

class child extends Super
{ 
    public function printHello() {
        echo "阿凡提de小毛驴";
    }

    // 声明一个公有方法,调用上面的私有方法 printHello
    public function printTest() {
        $this->printHello();
    }
}

$super = new Super();
echo $super->printTest();
echo '------------------------------------'.PHP_EOL;
$child = new child();
echo $child->printTest();

此时结果输出如下。我想此种情况是最没有疑问的,也是符合预期的。

string(5) "Super"
array(2) {
  [0] =>
  string(10) "printHello"
  [1] =>
  string(9) "printTest"
}
Super hello
------------------------------------
阿凡提de小毛驴

此问题已在我在本网站发起的问答中得到了解答

PHP 中 $this 在继承时引发的问题?

参考文章:
php继承相关的一个问题
PHP静态绑定与动态绑定
静态绑定和动态绑定的区别
关于php面向对象动态绑定和静态绑定的理解


kinra
19 声望0 粉丝