Skip to content

对象与引用

概述

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
        );
    }
}

参考链接