PHP类的秘密(三) 可见度, 继承与重写

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

前两节介绍类构成要素的时候, 都提到了两个东西: 可见度和继承.

成员变量, 成员方法和类常量这三者有都可以被继承, 而可见度又决定了这三者的继承效果. 所以我们把可见度和继承放在一起讲.

基础知识

先说继承extend, 继承是子类自动共享父类属性和方法的机制,这是类之间的一种关系。

父类 − 一个类被其他类继承,可将该类称基类, 有根基, 基础的意思。

子类 − 一个类继承其他类称为子类,也可称为派生类, 是对父类的延申.

关于继承, 有以下几个基本规则:

  1. PHP 不支持多重继承,一个类只能继承一个父类, 但一个父类可以又多个子类.

  2. 子类也可以是其他类的父类. PHP中支持多级继承.

  3. 父类必须在子类之前被声明。此规则适用于类继承其它类与接口。

下面这个例子表达了子类与父类的关系, 以及多级继承:

class A {}
class B extends A {}
class C extends B {}
class D extends B {}
$someObj = new A();  
$someOtherObj = new B(); 
$anotherObj = new C();
$lastObj = new D();

//B是A的子类, C和D都是B的子类, 那么C和D同样也是A的子类
复制代码

再让我们重温一下可见度 Visibility 的限制条件:

  1. public, 就是可以公开的, 没有必要隐藏的数据信息, 可以在程序中的任何位置被其他类和对象调用, 子类可以继承和使用父类中的所有公共成员

  2. private私有的,被 private 关键字修饰的成员变量和成员方法, 只能在所属类的内部被调用和修改,不可以在类外被访问, 在子类中也不可以被调用或修改

  3. protected受保护的, 修饰的成员变量和方法, 只能被其自身以及其子类和父类的内部访问

  4. 如果没有写可见度关键词, 那么系统会默认为public

那么我们可以获知, 子类能继承的只有父类非私有的属性和方法. 我们看下面这个例子:

class Woshinibaba
{
    private $private='fatherprivate';       //定义私有属性
    protected $protected='fatherprotected';   //定义受保护的属性
    public $public='fatherpublic';       //定义公共属性

    private function fatherprivate()           //定义私有方法
    {
    echo $this->private;                 //访问私有属性;
    echo $this->protected.'<br>';        //访问受保护属性;
    echo $this->public.'<br>';            //访问公共属性;
    }

    protected function fatherprotected()      //定义受保护的方法
    {
    echo $this->private;
    echo $this->protected.'<br>';
    echo $this->public.'<br>';
    }

    public function fatherpublic()            //定义公共的方法
    {
    echo $this->private;
    echo $this->protected.'<br>';
    echo $this->public.'<br>';
    }
}

class Test7 extends Woshinibaba
{
    public function childpublic()        //定义子类公共方法
    {
    echo $this->public;                  //访问父类公共属性
    echo $this->protected;             //访问父类受保护属性
    echo $this->private;             //访问父类私有属性
    }

    function childtest()              //默认为public属性
    {
    parent::fatherprotected();          //访问父类受保护方法
    parent::fatherprivate();            //报错: 子类无法访问父类私有方法
    }
}

$child= new Test7();      //新建子类对象;
$child->fatherprivate();  //报错, 无法在类外访问父类私有方法;
$child->fatherprotected();  //报错, 无法在类外访问父类受保护方法;
$child->fatherpublic():    //输出fatherprivate fatherprotected fatherpublic, 父类方法可以访问自身的private和protected属性;

$child->childpublic();   //输出fatherpublic fatherprotected, 以及Undefined property:$private, 因为无法从子类访问父类的private变量;

$child->childtest();     //只有fatherprotected()可以正常运行;
echo $child->public;    //fatherpublic, 公共属性可以在任意位置访问;
echo $child->protected;    //报错Cannot access protected property, 无法在类外访问protected属性;
echo $child->private;     //报错Undefined property, 无法访问父类私有属性;
复制代码

从上面的例子中, 还有2个关键点:

  1. 子类可以使用parent::这个代词来访问父类protected方法, parent这里指代当前方法所在类的父类;
  2. 类的内部指class{}以内的代码, 类的外部指其之外的代码, protected方法无法在类外被调用;

重写override

子类除了可以继承父类的非private成员, 还可以通过用同样的名字覆盖父类的成员, 已实现不同的功能, 这就是重写。子类可以根据自己的情况, 对父类的属性和方法进行重写, 或增加只属于自己的属性和方法.

方法重写

我们先来举例看一下子成员方法重写:

class Base1                          
{   //公共方法, 包含2个强制参数, 1个可选参数
    public function foo(int $a, $b, $c='test') { }   
    final public function bar()        //定义final方法
    {
        echo 'test';
    }
}

class Extend1 extends Base1  
{      //子类可以将父类的必填参数改为可选参数, 并新增可选参数, 修改参数默认值, 可以删除参数类型, 并且制定返回类型
    function foo($a, $b=10, $c='done', array $d=[]): float
    {
        return $DDD=$a*$b;            //子类重写方法体
    }
    function milk()
    {
        parent::foo();            //子类依然可以调用父类方法;
    }
}

class Extend2 extends Base1    //报错, 不能将参数$a类型改为float;
{
    function foo(float $a=5, $b, $c='test') {}   
}

class Extend3 extends Base1
{
    function foo(int $a, $b, $c) {}   //报错, 不能将可选参数$c变为必填参数
    function bar()                //报错, 无法重写父类final方法;
    {
    echo 'done';
    }
}

class Extend4 extends Base1
{      //报错, 不能将父类public方法重写为protected
    protected function foo(int $a, $b, $c=10) {}   

}

class Extend5 extends Base1
{      //报错, 不能将父类非static方法重写为static
    static public function foo(int $a, $b, $c=10) {}
}
复制代码

好的, 我们根据上面的例子得出重写的基本规则:

  1. 当子类重写父方法时,子类可以将父类强制参数改为可选参数, 子类可以新增可选参数;

  2. 当子类重写父方法时,子类不能新增强制参数, 不能移除参数、不能修改可选参数为强制参数, 不能将参数类型改为其他类型, 但却可以删除父类的参数类型;

  3. 当子类重写父方法时,子类方法返回值类型必须和父类的保持一致。如果父类没有指定返回类型,那么子方法可以指定自己的返回类型。

  4. 子类重写父类方法时, 如果父类方法是public则子类方法只能是public. 如果父类方法是protected, 则子类可以是public或protected. 我上面的例子不太全, 大家可以自己在本地测试一下.

  5. 子类重写父类方法后, 依然可以通过 parent:: 来访问被覆盖的方法。

  6. 如果父类“很强势”,不想自己的方法被子类覆盖重写,可以使用final关键字来修饰该方法。

  7. 子类重写父类方法时, 子类方法的static状态必须和父类一致, 如果父类是static, 则子类方法必须也是static, 如果父类是non-static, 则子类也必须是non-static; 关于静态的详细知识我们下一节讲.

总结一下就是子类在重写方法的时候, 对于参数, 可见度的限制条件一定要和父类一致或更宽松(weaker). 返回值除外, 子类的返回值可以比父类更严格.

我们再看当父类的方法可见度是private的时候, 以下情况都是正常的:

class Baro
{    //定义父类private方法
private function testPrivate() {}
}

class Fool extends Baro
{    //定义子类同名方法 protected可见度, 并增加强制参数$a
protected function testPrivate($a){}
}

class fool1 extends Baro
{    \\定义子类同名方法 private可见度
private function testPrivate($b){}
}

class fool2 extends Baro
{  //定义子类同名方法, public可见度, 并增加返回类型
public function testPrivate($d):array{}
}
复制代码

通过上面这个例子, 父类定义了私有方法, 而父类的私有方法是无法继承给子类的, 所以子类可以做任意的重写(新增), 完全没有限制.

属性重写

子类重写父类属性时, 遵循一个原则: 子类属性的可见度必须和父类一致或更宽松.

我们来看个实例:

class MyClass1
{
    public $public = 'Public';          
    protected $protected = 'Protected';
    private $private = 'Private';

    function printHello()
    {
        echo $this->public;
        echo $this->protected;
        echo $this->private;
    }
}

$obj = new MyClass1();
echo $obj->public; // 这行能被正常执行
echo $obj->protected; //报错: 无法在类外直接访问受保护的属性
echo $obj->private; //报错: 无法在类外直接访问私有的属性
$obj->printHello(); //PublicProtectedPrivate, 均可以在本类内部访问

class MyClass2 extends MyClass1
{
    private $private = 'Private2';     //新增私有属性, 正常
    public $public = 'Public2';        //重写父类公共属性, 正常
    protected $protected = 'Protected2';         //重写父类protected属性, 正常

    function printHello()
    {
        echo $this->public;
        echo $this->protected;
        echo $this->private;
    }
}

$obj2 = new MyClass2();
$obj2->printHello(); //Public2 Protected2, Private2;

class MyClass3 extends MyClass1
{
    public $private = 'Private3'      //正常
    protected $public = 'Public3';     //报错: 可见度必须是public
    public $protected = 'Protected3';     //正常, 可以重写为public

class MyClass4 extends MyClass1
{
    static public $public = 'Public4';     //报错: 无法重写为static属性;
复制代码

好的, 到这里我们总结一下属性重写的规则:

  1. 子类的static属性必须和父类保持一致;

  2. 如果父类属性是public, 则子类属性只能是public;

  3. 如果父类属性是protected, 则子类可以是public 或 protected;

  4. 如果父类属性是private, 则子类可以是任何属性, 因为父类私有属性在子类是不可见的, 所以不会影响子类新建同名属性.

父类Private属性

但如果我们把上例中的$obj2打印出来, 会发现这里面有一个坑:

print_r($obj2);    
//output:
MyClass2 Object 
( [private:MyClass2:private] => Private2     //子类同名私有属性
[public] => Public2 
[protected:protected] => Protected2 
[private:MyClass1:private] => Private )    //父类私有属性同时存在
复制代码

可以看到, 作为子类对象, 它包含了两个同名属性attr, 一个是它自己的私有属性, 另一个实际上是继承了父类private属性. 而父类其他类型的属性都被覆盖重写看不到了. 也就是说只有父类的私有变量是不会被子类覆盖重写的, 并且也会被子类继承.

那么此时当子类对象被父类方法调用, 会出现什么情况? 我们看下面这个例子:

class POP
{       //定义父类私有属性, 以及公共方法打印属性;
    private $attr = "father";
    public function getFields()
    {
        echo get_class($this);
        var_dump($this);
        return $this->attr;
    }
}

class COC extends POP
{     //定义子类同名属性, 并继承父类方法;
    protected $attr = "Child";
}

$obj = new COC();
var_dump($obj->getFields());   //调用父类方法, output:father
复制代码

上面这个例子中, 只要父类的属性是private, 不管子类属性可见度是什么, 输出的都是”father”, 这与常理不符. 是$this发生错乱失效了么?

当然不是, 从$obj打印信息中我们知道, 当父类属性是private时, 子类都会继承并保留这个父类的值, 而且不会被覆盖重写. 此时对象中存在两个同名属性attr.

两个同名属性, 该如何输出呢? PHP的解决方案是: 当被父类方法调用时, $this->attr指代的对象就是父类私有变量, 当子类方法调用时, 就返回子类对应属性”Child”, 也就是说, 子类方法调用时, 即便会继承父类私有属性, 但$this->attr仍然指向子类的属性. 这种机制可以理解为”就近原则“:

当对象含有两个同名属性的时候, 通过哪个类调用, 就返回那个类的属性.

最后还有一点, 我们上面讲过子类方法可以通过parent::来访问被覆盖的父类方法, 但子类却无法通过parent::来访问被覆盖的父类属性, 除非父类属性是静态的. 好了, 今天说了不少关于静态的事情, 我们下一节就讲讲PHP类中的静态属性和方法.

下课~!

总结不易, 请勿私自转载, 否则别怪老大爷不客气!

参考资料:

www.php.net PHP官方文档

零基础PHP学习笔记 明日科技

PHP父类方法里如何访问子类属性 www.zhai14.com/blog/how-to… 翟码农

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享