对象与引用
概述
PHP 中对象默认通过引用传递(更准确地说是"引用标识"或"handle"传递),而非值复制。这意味着将一个对象赋值给另一个变量时,两个变量指向同一个对象实例。要创建对象的独立副本,需要使用 clone 关键字。理解 PHP 的对象引用机制对于避免意外的副作用至关重要。
基础概念
PHP 对象模型的核心概念
- 对象变量存储的是对象的"标识符"(handle/pointer),而非对象本身
- 赋值操作复制的是标识符,两个变量指向同一对象
clone创建对象的浅拷贝(新对象,属性值复制)- 对象内部的引用计数用于自动垃圾回收
语法与代码
对象默认引用传递
php
<?php
declare(strict_types=1);
class User
{
public string $name;
public int $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
}
$a = new User('Alice', 30);
$b = $a; // $a 和 $b 指向同一对象
$b->name = 'Bob';
echo $a->name; // "Bob" — $a 也被修改了
var_dump($a === $b); // true — 同一实例clone 创建副本
php
<?php
declare(strict_types=1);
$a = new User('Alice', 30);
$b = clone $a; // $b 是 $a 的独立副本
$b->name = 'Bob';
echo $a->name; // "Alice" — $a 未受影响
echo $b->name; // "Bob"
var_dump($a === $b); // false — 不同实例
var_dump($a == $b); // true — 属性值相同函数参数中的对象传递
php
<?php
declare(strict_types=1);
function modifyUser(User $user): void
{
$user->name = 'Modified';
}
$original = new User('Original', 25);
modifyUser($original);
echo $original->name; // "Modified" — 对象通过引用传递__clone 魔术方法
php
<?php
declare(strict_types=1);
class Document
{
public string $title;
/** @var Tag[] */
public array $tags = [];
private string $id;
public function __construct(string $title)
{
$this->title = $title;
$this->id = uniqid('doc_');
}
public function addTag(string $name): void
{
$this->tags[] = new Tag($name);
}
public function __clone(): void
{
// clone 时重新生成 ID
$this->id = uniqid('doc_');
// 深度克隆 tags 数组
$clonedTags = [];
foreach ($this->tags as $tag) {
$clonedTags[] = clone $tag;
}
$this->tags = $clonedTags;
}
public function getId(): string
{
return $this->id;
}
}
class Tag
{
public function __construct(public readonly string $name) {}
}
$doc1 = new Document('PHP Tutorial');
$doc1->addTag('PHP');
$doc1->addTag('Programming');
$doc2 = clone $doc1;
echo $doc1->getId(); // doc_abc123
echo $doc2->getId(); // doc_def456 — 不同的 ID
echo $doc1->tags[0]->name; // PHP
echo $doc2->tags[0]->name; // PHP引用计数与 unset
php
<?php
declare(strict_types=1);
$obj = new User('Alice', 30);
$ref1 = $obj;
$ref2 = $obj;
// 引用计数为 3 ($obj, $ref1, $ref2)
unset($ref2);
// 引用计数变为 2
unset($ref1);
// 引用计数变为 1
unset($obj);
// 引用计数变为 0,对象被销毁浅拷贝 vs 深拷贝
php
<?php
declare(strict_types=1);
class Address
{
public function __construct(public string $city) {}
}
class Person
{
public function __construct(
public string $name,
public Address $address
) {}
}
$person1 = new Person('Alice', new Address('Beijing'));
// 浅拷贝 - address 属性仍然引用同一对象
$person2 = clone $person1;
$person2->name = 'Bob';
$person2->address->city = 'Shanghai';
echo $person1->address->city; // "Shanghai" — 被修改了!
// 深拷贝 - 所有嵌套对象也被克隆
class DeepCopyablePerson extends Person
{
public function __clone(): void
{
$this->address = clone $this->address;
}
}
$person3 = new DeepCopyablePerson('Alice', new Address('Beijing'));
$person4 = clone $person3;
$person4->address->city = 'Shanghai';
echo $person3->address->city; // "Beijing" — 未受影响详细说明
对象标识与引用的区别
PHP 中对象变量不是 C++ 意义上的"引用"(&),而是"对象标识符"的复制:
php
<?php
declare(strict_types=1);
$a = new User('Alice', 30);
$b = $a; // 复制标识符,两个变量指向同一对象
$c = &$a; // 引用绑定,$a 和 $c 是同一变量
unset($a); // $b 仍然可以访问对象
// $c 也不能访问了(引用绑定被销毁)
echo $b->name; // "Alice"PHP 引用(&)与对象引用的区别
php
<?php
declare(strict_types=1);
class Counter
{
public int $count = 0;
}
// 对象引用(默认行为)
$a = new Counter();
$b = $a; // $b 是 $a 的对象引用
$b->count = 5;
echo $a->count; // 5
// PHP 引用(&)
$x = new Counter();
$y = &$x; // $y 是 $x 的别名
$y = new Counter(); // $x 也被替换
echo $x->count; // 0(新对象)对象在数组中的引用行为
php
<?php
declare(strict_types=1);
class Item
{
public function __construct(public string $name) {}
}
$item = new Item('Original');
$items = [$item, $item, $item];
// 三个元素指向同一对象
$items[0]->name = 'Changed';
echo $items[1]->name; // "Changed"
echo $items[2]->name; // "Changed"实战示例
场景一:不可变对象模式
php
<?php
declare(strict_types=1);
class ImmutableUser
{
public function __construct(
public readonly string $name,
public readonly int $age
) {}
public function withName(string $newName): self
{
return new self($newName, $this->age);
}
public function withAge(int $newAge): self
{
return new self($this->name, $newAge);
}
}
$original = new ImmutableUser('Alice', 30);
$modified = $original->withName('Bob');
echo $original->name; // "Alice"
echo $modified->name; // "Bob"场景二:深拷贝工具函数
php
<?php
declare(strict_types=1);
function deepClone(object $object): object
{
return unserialize(serialize($object));
}
class TreeNode
{
public function __construct(
public string $value,
public ?TreeNode $left = null,
public ?TreeNode $right = null
) {}
}
$tree = new TreeNode(
'root',
new TreeNode('left'),
new TreeNode('right')
);
$clonedTree = deepClone($tree);
$clonedTree->left->value = 'modified_left';
echo $tree->left->value; // "left" — 原树不受影响
echo $clonedTree->left->value; // "modified_left"注意事项
注意事项
- PHP 对象默认是"引用传递"(实际上是标识符复制),不是值传递
clone只进行浅拷贝,嵌套对象仍然是引用关系- 使用
__clone()魔术方法实现深拷贝逻辑 serialize()/unserialize()可以实现深拷贝,但不处理闭包/资源
小贴士
- 当需要独立副本时,务必使用
clone - 对于包含嵌套对象的类,实现
__clone()进行深度克隆 - 使用
readonly属性创建不可变对象,避免引用问题的风险
最佳实践
1. 对不可变数据使用 readonly 属性
php
<?php
declare(strict_types=1);
class ValueObject
{
public function __construct(
public readonly string $value
) {}
}2. 实现 __clone 处理嵌套对象
php
<?php
declare(strict_types=1);
class Composite
{
public function __construct(
public readonly string $name,
private array $children = []
) {}
public function addChild(self $child): void
{
$this->children[] = $child;
}
public function __clone(): void
{
$this->children = array_map(
fn(self $child) => clone $child,
$this->children
);
}
}