PHP的类和魔术方法

文章目录

环境

  • PHP 8.4.10
  • Windows 11 家庭版

类的定义

PHP的类(class)和Java的类有很多相似之处,也有很多不同之处。

下面是一个简单的PHP类:

php 复制代码
class Person {
    private $name;
    private $age;

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function getAge() {
        return $this->age;
    }

    public function setAge($age) {
        $this->age = $age;
    }

    public function hello() {
        echo 'My name is ' . $this->name . '. I am ' . $this->age . ' years old.';
    }
}

对象引用

PHP和Java类似,对于对象,采用的是"引用"策略。比如:

php 复制代码
$x = new Person();
$x->setName('Tom');
$x->setAge(30);

$x->hello(); // My name is Tome. I am 30 years old.

$y = $x; // 引用同一对象

$y->setName('Jerry');
$y->setAge(20);

$x->hello(); // My name is Jerry. I am 20 years old.

运行结果如下:

powershell 复制代码
My name is Tome. I am 30 years old.
My name is Jerry. I am 20 years old.

可见, $x$y 指向了同一个对象,这一点和Java是相同的。

需要注意的是,在Java中,"一切都是对象",除了primitive类型的变量(比如int、double),其它全都是对象。

例如,在Java中,数组是对象,而在PHP中,数组并不是对象。因此,PHP在对数组变量赋值时,会把数组复制一份。如果希望两个数组变量指向同一个数组,则需要用 &

php 复制代码
$arr1 = array(1, 2, 3);

$arr2 = $arr1; // 把数组复制了一份
$arr3 = &$arr1; // 指向同一数组

echo $arr1[0] . PHP_EOL; // 1

$arr2[0] = "4";

echo $arr1[0] . PHP_EOL; // 1

$arr3[0] = "5";

echo $arr1[0] . PHP_EOL; // 5

魔术方法

PHP的魔术方法(Magic Methods)是PHP提供的一组特殊方法,它们以双下划线 __ 开头,允许开发者在特定事件发生时自动执行自定义逻辑(本质是回调函数)。

下面列举几个常用的魔术方法。

__construct()

即构造器,在创建PHP对象时,会自动调用 __construct() 方法。

构造器是面向对象中的一个重要概念。这里我们把PHP和Java的构造器做一下对比。

Java构造器规则为:

  • 类一定有构造器。如果没有显式提供构造器,则系统会自动提供一个无参构造器
  • 类可以有多个构造器,比如一个无参构造器和一个(或多个)带参构造器
  • 构造器一定要调用父类的构造器,且必须在构造器的第一行处:
    • 通过 super() 显式调用
    • 通过 this() 显式调用另一个构造器(另一个构造器和当前构造器的规则相同)
    • 若前两种情况都不满足,则系统会自动在第一行隐式调用 super() 即父类的无参构造器(如果父类没有无参构造器,则编译报错)

PHP构造器规则为:

  • 类可以没有构造器,系统也不会自动提供无参构造器
  • 类不能有多个构造器,不过可以通过默认参数,实现类似Java的多个构造器功能
  • 子类构造器不会隐式调用父类构造器,只能显式调用
  • 子类构造器可以在任意处调用父类构造器

总结:PHP和Java构造器的差异,可能是因为二者的设计理念不同:

  • Java:类必须有构造器,构造器第一行必须调用父类构造器,这种设计高度统一,非常严格和安全
  • PHP:类可以没有构造器,构造器可以不调用父类构造器,这种设计更加强调灵活性,约束少,适合快速开发

下面是PHP的构造器例子:

php 复制代码
class Animal {
    public function __construct() {
        echo "Animal constructor" . PHP_EOL;
    }
}

class Dog extends Animal {
    public function __construct() {
        parent::__construct();
        echo "Dog constructor" . PHP_EOL;
    }
}

$x = new Dog();

运行结果如下:

powershell 复制代码
Animal constructor
Dog constructor

__set() 和 __get()

如果访问不存在(或者私有不可访问)的属性时,就会触发 __get() 方法。

若没有 __get() 方法:

  • 对于不存在的属性,会报warning Undefined property
  • 对于私有不可访问的属性,会报错 Cannot access private property

如果设置不存在(或者私有不可访问)的属性时,就会触发 __set() 方法。

若没有 __set() 方法:

  • 对于不存在的属性,会动态添加该属性
  • 对于私有不可访问的属性,会报错 Cannot access private property
php 复制代码
class Person {
    private $name;
    private $age;

    public function __set($key, $value) {
        if ($key == 'name')
            $this->name = $value;
        else if ($key == 'age')
            $this->age = $value;
    }

    public function __get($key) {
        if ($key == 'name')
            return $this->name;
        else if ($key == 'age')
            return $this->age;
    }
}

$x = new Person();
$x->name = "Tom";
$x->age = 30;
echo $x->name . " ". $x->age; // Tom 30

本例中, nameage 是私有属性,无法直接访问,因此通过 __set()__get() ,实现对其设置和访问。

注意: __set()__get() 与显式的 getXxx()setXxx() 相比,灵活性好,代码简洁,但是安全性差,调试困难,性能略差。在实际项目中可根据需求和喜好来选择采用哪种方式。

注意:若没有 __set() 方法:

  • 对于不存在的属性,会动态添加该属性
  • 对于私有不可访问的属性,会报错 Cannot access private property
php 复制代码
class Person {
    // private $name; // 反注释这一行,就会报错
}

$x = new Person();

$x->name = 'Tom';

echo $x->name; // Tom

__isset()

如果 isset()empty() 方法检查的是不存在(或者私有不可访问)的属性,就会触发 __isset() 方法。

php 复制代码
class Person {
}

$x = new Person();

if (isset($x->name)) // false
    echo 'yes';
else
    echo 'no';

本例中,Person对象没有name属性,因此 isset() 方法返回false。

在Person类中添加如下代码:

php 复制代码
    public function __isset($name) {
        return true;
    }

注意,这里简单粗暴的把 __isset() 返回了true,只是为了演示其用法。

__unset()

如果 unset() 方法作用于不存在(或者私有不可访问)的属性,就会触发 __unset() 方法。

php 复制代码
class Person {
    private $data = [];

    public function __set($key, $value) {
        $this->data[$key] = $value;
    }

    public function __get($key) {
        if (array_key_exists($key, $this->data))
            return $this->data[$key];
        else
            return null;
    }

    public function __unset($key) {
        if (array_key_exists($key, $this->data))
            unset($this->data[$key]);
    }
}

$x = new Person();

$x->name = 'Tom';

echo $x->name; // Tom

unset($x->name);

echo $x->name; // nothing

__call()

如果调用了不存在(或者私有不可访问)的成员函数,就会触发 __call() 方法。

_call() 有两个参数:

  • 函数名
  • 参数列表,是一个数组
php 复制代码
class Person {
    public function __call($name, $arguments) {
        echo "Calling method '$name' with arguments: " . implode(", ", $arguments). PHP_EOL;
    }    
}

$x = new Person();

$x->f1(); // Calling method 'f1' with arguments: 

$x->f2(1); // Calling method 'f2' with arguments: 1

$x->f3(1, 2, 3); // Calling method 'f3' with arguments: 1, 2, 3

__callStatic()

__call() 类似,作用于静态方法。

__invoke()

可以把对象像函数一样调用。

php 复制代码
class Person {
    public function __invoke($arg1, $arg2) {
        return $arg1 + $arg2;
    }
}
    
$x = new Person();
echo $x(10, 20); // 30

__invoke() 的一个场景是,对于单一用途的类,可以用它来简化代码:

php 复制代码
class Log {
    public function __invoke($content) {
        // 记log
    }
}

$log = new Log();

$log('Hello, world!');

本例中,Log类只有一个方法,因此可以简化代码。

从简化代码的角度,和Java的Lambda有点类似,都使用了"匿名方法"。

__toString()

在对象被当做字符串使用时,会调用 __toString() 方法。

php 复制代码
class Person {
    public $name;
    public $age;
    public function __tostring() {
        return 'name: ' . $this->name . ', age: '. $this->age;
    }
}

$x = new Person();
$x->name = 'Tom';
$x->age = 30;

echo $x; // name: Tom, age: 30

注意:PHP类中,如果没有显式提供 __toString() 方法,在对象被当做字符串使用时,会报错 Object of class Person could not be converted to string ,这一点和Java不同。Java类中如果没有重写 toString() 方法,会返回 <类名>@<哈希码> 的字符串。

__clone()

前面提到,PHP是引用对象的,因此 $y = $x 会使得二者指向同一个对象。

如果想要复制一个对象,可以用 clone 。比如:

php 复制代码
class Person {
    public $name;
    public $age;
}

$x = new Person();
$x->name = 'Tom';
$x->age = 30;

$y = clone $x; // 克隆对象

$y->name = 'Jerry';
$y->age = 20;

echo $x->name. " ". $x->age; // Tom 30

echo $y->name. " ". $y->age; // Jerry 20

可见, $x$y 各自指向一个Person对象,互不影响。

乍一看,这样做似乎已经完美解决了问题:只要把源对象的各个属性值一一赋值给目标对象的相应属性,就万事大吉了。然而,如果需要克隆的对象,里面的数据里又包含了一个对象,这时就会出现"深拷贝还是浅拷贝"的问题。比如,一个人拥有一本书,现在要把这个人克隆一份,此时,我们希望这个人所拥有的书也克隆一份,而不是两个人拥有同一本书(两个Person对象的book属性指向同一个Book对象)。

下面的代码使用默认的 clone 方式:

php 复制代码
class Book {
    public $name;
}

class Person {
    public $name;
    public Book $book;
}

$p1 = new Person();
$p1->name = "Tom";
$b1 = new Book();
$b1->name = "三国演义";
$p1->book = $b1;

echo $p1->name . ', ' . $p1->book->name . PHP_EOL; // Tom, 三国演义

$p2 = clone $p1;
$p2->name = "Jerry";
$p2->book->name = "水浒传";

echo $p1->name . ', ' . $p1->book->name . PHP_EOL; // Tom, 水浒传

可见,把Person对象克隆后,新老对象的book属性指向了同一个Book对象。也就是说,PHP默认的克隆是"浅拷贝"。而很多情况下,我们想要的是"深拷贝"。

要想实现深拷贝,就得用到 __clone() 方法了。该方法在克隆对象时被回调,用于控制如何克隆对象。

在Person类中添加如下代码:

php 复制代码
    public function __clone() {
        $this->book = clone $this->book;
    }

该代码逻辑为,在克隆Person对象时,显式把book属性克隆一份,这样,新对象得到的就是克隆后的Book对象,而不是原来的Book对象了。

相关推荐
梦吉网络4 小时前
悬赏任务系统小程序/APP源码,推荐任务/发布任务/会员服务
php
hweiyu006 小时前
PHP之ThinkPHP5视频教程
php
一枚小小程序员哈7 小时前
基于PHP的快递管理系统的设计与实现
php
用户3074596982079 小时前
🌟 PHP 中的 `use` 关键字完全指南
php
PyHaVolask9 小时前
PHP进阶语法详解:命名空间、类型转换与文件操作
开发语言·php·composer
用户30745969820710 小时前
🌟 PHP 接口(Interface)完全入门指南
php
欧的曼14 小时前
cygwin+php教程(swoole扩展+redis扩展)
开发语言·redis·后端·mysql·nginx·php·swoole
小楓120118 小时前
後端開發技術教學(二) 條件指令、循環結構、定義函數
服务器·后端·php
做一位快乐的码农19 小时前
基于vue的财务管理系统/基于php的财务管理系统
php