PHP学习笔记13:类和对象 V
图源:php.net
Final关键字
final 关键字作用于类,可以让类不能被继承。作用于方法和常量,可以让方法和常量不能被重写。属性不能被声明为final 。
final 的常见用途是在模版方法模式中,将基类的骨架方法声明为final :
abstract class Control
{
protected bool $need_login = true;
protected array $header = [];
final public function handle_request()
{
$this->pre_handle();
$this->handle();
$this->after_handle();
}
...
这是PHP学习笔记11:类和对象 III中举的一个例子,handle_request 是所有Control 类的入口方法,同时也是基类的“骨架方法”,通常该方法不应当被子类重写,子类只应当重写pre_handle 、handle 、after_handle 。在这种情况下就可以将handle_request 方法声明为final 。
克隆对象
在普通情况下,对对象进行赋值得到的是一个原对象的引用:
<?php
class Pointer implements Stringable
{
public function __construct(private int $x, private int $y)
{
}
public function set_x(int $x)
{
$this->x = $x;
}
public function set_y(int $y)
{
$this->y = $y;
}
public function __toString(): string
{
return "({$this->x},{$this->y})";
}
}
$p1 = new Pointer(1, 6);
$p2 = $p1;
$p2->set_x(6);
$p2->set_y(10);
echo "Pointer1:{$p1}" . PHP_EOL;
echo "Pointer2:{$p2}" . PHP_EOL;
修改赋值后的对象同时也会改变原始对象,因为他们指向同一个对象。在一般情况下这么做是有意义的,可以节省内存空间。但有时候我们需要获取一个对象的“镜像”,而不是一个对象的引用。这样我们可以安全地修改“镜像”而不影响到原来的对象。
只要使用clone 关键字就能实现:
...
$p1 = new Pointer(1, 6);
$p2 = clone $p1;
$p2->set_x(6);
$p2->set_y(10);
echo "Pointer1:{$p1}" . PHP_EOL;
echo "Pointer2:{$p2}" . PHP_EOL;
需要注意的是,clone 关键字获取到的是一个对象的“浅拷贝”,也就是说是对每个对象属性进行复制,但对象属性如果也是一个引用,就不会对引用的数据进行拷贝,而是使用同一份引用。
在PHP学习笔记4:变量中我展示过一个二叉树的示例,这里用二叉树进行说明:
...
$root2 = clone $root;
$root2->left->context = 'root2>left';
require_once '../util/array.php';
$contexts = [];
treeTraverse($root, $contexts);
print_arr($contexts);
$contexts = [];
treeTraverse($root2, $contexts);
print_arr($contexts);
完整代码见代码仓库。
可以看到修改了$root2 中的根节点的左子节点内容后,$root1 相应节点也改变了。这是因为二叉树的属性都是引用值,通过clone 获取到的拷贝$root2 的属性引用值与$root1 完全相同。
要让对象的属性能进行“深拷贝”,我们需要实现__clone 魔术方法:
...
public function __clone()
{
if (!is_null($this->left)) {
$this->left = clone $this->left;
}
if (!is_null($this->right)) {
$this->right = clone $this->right;
}
}
...
__clone 方法将在使用clone 创建了一个对象拷贝后,在新的拷贝上调用。利用__clone 魔术方法,我们在新的Node 对象创建后,让其左子节点和右子节点分别进行拷贝,并赋值。这样获取到的新的二叉树就是原始二叉树的一个深拷贝,而非浅拷贝。最后的测试结果也说明了这一点。
对象比较
使用操作符== 比较对象的规则是,两个对象的属性都相等(使用== )且对象也属于同一个类,则两个对象相等。使用=== 比较对象则需要两个对象是同一个实例的引用:
<?php
require_once './pointer.cls.php';
$p1 = new Pointer(1, 5);
$p2 = new Pointer(1, 5);
var_dump($p1 == $p2);
var_dump($p1 === $p2);
$p3 = $p2;
var_dump($p3 == $p2);
var_dump($p3 === $p2);
后期静态绑定
在类定义中self 可以代替当前类名,在大部分情况下这么做并没有什么问题,但某些时候就会产生一些意外:
<?php
class Base
{
public static function test(): void
{
echo "Base::test() is called." . PHP_EOL;
}
public static function call_test()
{
echo "Base::call_test() is called." . PHP_EOL;
self::test();
}
}
class Child extends Base
{
public static function test(): void
{
echo "Child::test() is called." . PHP_EOL;
}
}
Child::call_test();
在这个示例中,Child 继承了Base 的Call_test 方法,重写了test 方法。按一般的想法,继承的call_test 方法中self::test() 应当调用Child 重写的test 方法才符合继承的原则,但现实情况是调用了父类的test 方法。
这里有两个概念,Child::call_test() 这种指定类名调用静态方法的行为叫做“非转发调用”,即明确的,不需要解释器经过转发来确定具体要由哪个类来执行的调用方式。而self::test() 这样的,需要根据当前上下文情况来判断需要由哪个类来执行调用的,叫做“转发调用”。
对self::test() 而言,它只会“死板”地将self 替换为所在类的类名然后调用静态函数,也就是说在Base 中,self::test() 会固定地被解析为Base::test() ,即使它被继承后也是遵循这样的行为。这也是为什么会出现上面的现象。
要改变这种情况,要使用static 而非self :
<?php
class Base
{
...
public static function call_test()
{
echo "Base::call_test() is called." . PHP_EOL;
static::test();
}
}
...
Child::call_test();
static 的运行机制是,会记录上一个“非转发调用”时Child::call_test 的类名Child ,在执行static::test 时,会用记录的调用类去执行具体调用,即执行Child::test 。
再看一个摘抄自官方手册的更复杂的例子:
<?php
class A
{
public static function foo()
{
static::who();
}
public static function who()
{
echo __CLASS__ . "\n";
}
}
class B extends A
{
public static function test()
{
A::foo();
parent::foo();
self::foo();
}
public static function who()
{
echo __CLASS__ . "\n";
}
}
class C extends B
{
public static function who()
{
echo __CLASS__ . "\n";
}
}
C::test();
这个例子的运行过程是,C::test 执行到B::test ,A::foo 被执行,static::who 被执行,这里static 会使用上一个非转发调用的类来替换,即A 。所以A::who 将被执行,所以会输出一个A 。然后B::test 中的parent::foo 被执行,也就是A::foo 被执行,static::who 被再次执行,再次寻找上一个非转发调用的类名,也就是C::test ,所以会输出一个C 。以此类推。
需要注意的是,上边所说的“上一个”,不是历史记录,而是“调用栈”中的上一个。
对象和引用
php中的对象都是引用,用C++的方式来解释就是对象变量实际上保存的是一个指向真实对象的指针。比较奇怪的是,在php中,可以将“引用的引用”当作原始对象来进行使用:
<?php
require_once './pointer.cls.php';
$p1 = new Pointer(1, 6);
$p2 = $p1;
$p3 = &$p1;
$p3->set_x(9);
$p4 = &$p3;
$p5 = &$p4;
echo "p1:{$p1}".PHP_EOL;
echo "p2:{$p2}".PHP_EOL;
echo "p3:{$p3}".PHP_EOL;
echo "p4:{$p4}".PHP_EOL;
echo "p5:{$p5}".PHP_EOL;
这里$p1 是指向真实对象的指针,$p3 是指向$p1 的指针,$p4 是指向$p3 的指针,$p5 是指向$p4 的指针。但奇怪的是它们都能正常的执行Pointer::__toString() 实现输出。大概是因为php解释器可以正确解析这种“指针链”?
序列化
可以使用serialize 和unserialize 将对象序列化和反序列化,这是一种代码持久化保存的技术。相关讨论在PHP学习笔记12:类和对象IV中已经有过讨论,所以不再重复举例说明。
更多说明见官方手册对象序列化。
协变与逆变
协变与逆变的内容比较广泛,不容易理解,所以请自行查找或者前往官方手册协变与逆变。
往期内容
|