文章目录
- 环境
- 类
- 魔术方法
-
- __construct()
- [__set() 和 __get()](#__set() 和 __get())
- __isset()
- __unset()
- __call()
- __callStatic()
- __invoke()
- __toString()
- __clone()
环境
- 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
本例中, name
和 age
是私有属性,无法直接访问,因此通过 __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对象了。