PHP之魔术方法

@爱耍流氓的唐僧  August 21, 2020

1.概述
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。

2.魔术方法
__construct构造方法,当一个对象创建时调用此方法,可以完成一些初始化的工作

__destruct 析构方法,PHP将在对象被销毁前(即从内存中清除前)调用这个方法

__call当调用一个未定义的方法是调用此方法,这里的未定义的方法包括没有权限访问的方法

__callStatic它的工作方式类似于 __call() 魔术方法,__callStatic() 是为了处理静态方法调用

__get当调用一个未定义的属性时访问此方法

__set给一个未定义的属性赋值时调用,这里的没有声明包括当使用对象调用时,访问控制为proteced,private的属性(即没有权限访问的属性)

__isset当在一个未定义的属性上调用isset()函数时调用此方法

__unset当在一个未定义的属性上调用unset()函数时调用此方法,与__get方法和__set方法相同,这里的没有声明包括当使用对象调用时,访问控制为proteced,private的属性(即没有权限访问的属性)

__sleep序列化的时候使用

__wakeup反序列化时候使用
serialize() 检查类中是否有魔术名称 __sleep 的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。
使用 __sleep 的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。
相反地,unserialize() 检查具有魔术名称 __wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。
使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。

__toString方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时,如果类没有实现此方法,则无法通过echo打印对象,会显示:Catchable fatal error: Object of class test could not be converted to string in
此方法必须返回一个字符串

__invoke当尝试以调用函数的方式调用一个对象时,__invoke 方法会被自动调用。

__set_state当调用 var_export() 导出类时,此静态 方法会被调用。

<?php

class A
{
    public $var1;
    public $var2;

    public static function __set_state($an_array) // As of PHP 5.1.0
    {
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';

eval('$b = ' . var_export($a, true) . ';'); // $b = A::__set_state(array(
                                            //    'var1' => 5,
                                            //    'var2' => 'foo',
                                            // ));
var_dump($b);

?>

__clone对象赋值是使用的引用赋值,如果想复制一个对象则需要使用clone方法,在调用此方法是对象会自动调用__clone魔术方法,如果在对象复制需要执行某些初始化操作,可以在__clone方法实现。
证明1引用赋值

<?php 
$carA = new stdClass();
$carA->brand = '奔驰';
$carA->power = '汽油';

$carB = $carA;
$carB->brand = '宝马';

var_dump($carA);
var_dump($carB);

1.png
可以看到,对 $carB 属性值的修改会污染 $carA 的属性值,这是 PHP 新手在循环代码中做对象赋值时经常会犯的错误,而且迭代次数多了之后不易察觉,要避免这个问题,可以借助 clone 关键字拷贝一个全新的对象来实现:

<?php

$carA = new stdClass();
$carA->brand = '奔驰';
$carA->power = '汽油';
$carB = clone $carA;
$carB->brand = '宝马';

var_dump($carA);
var_dump($carB);

2.png
说明 $carB 确实和 $carA 已经完全独立了,属性值的修改互不影响,但果真如此吗?我们增加点复杂度,现在在对象上新增对象属性:

<?php
$engine = new stdClass();
$engine->num = 4;

$carA = new stdClass();
$carA->brand = '奔驰';
$carA->power = '汽油';
$carA->engine = $engine;

$carB = clone $carA;
$carB->brand = '领克02';
$carB->power = '电池';
$carB->engine->num = 3;

var_dump($carA);
var_dump($carB);

3.png
这个时候,你会发现虽然通过 clone 拷贝的对象普通属性不再相互污染,但是嵌套的对象属性依然存在这个互相影响的问题,因此,我们把引用赋值和 clone 拷贝统统称之为「浅拷贝」,只有嵌套的对象属性也不相互污染的拷贝才是真正相互对立的「深拷贝」。要实现这种深拷贝,就要用到我们前面提到的 __clone 魔术方法。

但是 stdClass 显然也不支持这种类方法,因此,需要鸟枪换炮,换成真正的类来演示:

<?php

class Engine
{
    public $num = 4;
}

class Car
{
    public $brand;
    public $power;
    /**
     * @var Engine
     */
    public $engine;

    public function __clone()
    {
        $this->engine = clone $this->engine;
    }
}

$benz = new Car();
$benz->brand = '奔驰';
$benz->power = '汽油';
$engine = new Engine();
$benz->engine = $engine;

$lnykco02 = clone $benz;
$lnykco02->brand = '领克02';
$lnykco02->power = '电池';
$lnykco02->engine->num = 3;

var_dump($benz);
var_dump($lnykco02);

4.png
可以看到,无论是普通属性,还是嵌套对象属性,都已经完全独立,不再相互干扰,从而实现了真正意义上的深拷贝。

__debugInfo该方法在var_dump()类对象的时候被调用,如果没有定义该方法,则var_dump会打印出所有的类属性

<?php
class C {
    private $prop;
 
    public function __construct($val) {
        $this->prop = $val;
    }
 
    public function __debugInfo() {
        return [
            'propSquared' => $this->prop ** 2,
        ];
    }
} 
var_dump(new C(42));

object(C)#1 (1) {
  ["propSquared"]=>
  int(1764)
}

添加新评论