Skip to content

取消引用

概述

unset() 是 PHP 中断开引用关系的主要手段。当对一个持有引用的变量调用 unset() 时,它只是断开该变量名与数据的关联,并不会销毁数据本身,也不会影响其他引用同一份数据的变量。理解 unset() 在引用场景下的行为,对于避免意外的副作用和正确管理内存至关重要。

核心要点

unset() 断开的是变量名与值之间的绑定,而非销毁值本身。只要还有其他变量引用该数据,数据就继续存在。

基础概念

unset 的基本作用

unset() 销毁指定的变量,释放变量名与值之间的关联。对于普通变量,unset() 后变量不再存在;对于引用变量,unset() 只断开当前变量名的引用。

unset 与引用的关系

当多个变量引用同一份数据时,对其中一个变量执行 unset(),只会断开该变量与数据的关联。其他变量仍然可以正常访问数据。

unset 与 null 赋值的区别

unset()$var = null 是不同的操作:unset() 销毁变量,$var = null 将变量设为 null 但变量仍然存在。

语法与代码

unset 断开引用

php
<?php
declare(strict_types=1);

$a = 'shared data';
$b = &$a;

echo "unset 前: a={$a}, b={$b}\n";
// unset 前: a=shared data, b=shared data

unset($b); // 只断开 $b 的引用

echo "unset 后: a={$a}\n";     // unset 后: a=shared data
echo isset($b) ? 'b exists' : 'b not exists';
// b not exists — $b 已被销毁

不影响其他引用

php
<?php
declare(strict_types=1);

// 三个变量引用同一数据
$x = 'hello';
$y = &$x;
$z = &$y;

// unset $y — 只断开 $y,不影响 $x 和 $z
unset($y);

echo $x; // 输出: hello
echo $z; // 输出: hello
echo $x === $z ? 'same' : 'different'; // 输出: same

// 修改 $z,$x 同步变化
$z = 'world';
echo $x; // 输出: world

unset null 变量

php
<?php
declare(strict_types=1);

// unset 一个值为 null 的变量
$value = null;
$ref = &$value;

unset($ref); // 断开 $ref 的引用

echo $value; // 输出: (空)— 但 $value 仍然存在,值为 null
echo $value === null ? 'true' : 'false'; // 输出: true
echo isset($value) ? 'set' : 'not set';  // 输出: not set(null 值不被 isset 检测)

isset vs null

isset() 返回 false 当变量为 null。使用 array_key_exists()$var === null 来区分"变量不存在"和"变量值为 null"。

unset 与 foreach 中的引用

php
<?php
declare(strict_types=1);

$data = ['red', 'green', 'blue'];

foreach ($data as &$color) {
    echo strtoupper($color) . "\n";
}
unset($color); // 务必 unset,防止后续代码意外修改 $data[2]

// 如果没有 unset $color,下面的赋值会修改 $data[2]
$color = 'yellow'; // 有了 unset,这只是一个新变量
echo $data[2]; // 输出: blue(而非 yellow)

unset 对数组中引用元素的影响

php
<?php
declare(strict_types=1);

$arr = ['a', 'b', 'c'];
$ref = &$arr[1]; // $ref 引用 $arr[1]

unset($ref);    // 断开 $ref,不影响 $arr[1]

echo $arr[1]; // 输出: b — 数组元素完好

详细说明

unset 的内部行为

当调用 unset($var) 时,PHP 的内部操作如下:

  1. 减少变量名所在符号表条目的引用计数
  2. 如果引用计数降为 0,释放对应的 zval
  3. 从符号表中移除该变量名

对于引用变量,unset() 只移除当前变量名的绑定,不修改 is_ref 标志或其他变量的绑定状态。

unset vs = null

php
<?php
declare(strict_types=1);

// --- unset ---
$a = 'data';
$b = &$a;
unset($a);
// $a 不存在,$b 仍然指向 'data'

// --- = null ---
$x = 'data';
$y = &$x;
$x = null;
// $x 存在但值为 null,$y 也是 null(因为它们引用同一份数据)

echo isset($x) ? 'set' : 'not set';  // not set(null 不被 isset 识别)
echo isset($y) ? 'set' : 'not set';  // not set
echo $y === null ? 'true' : 'false'; // true
操作对变量的影响对引用的影响内存
unset($a)变量不存在断开该变量名的引用可能释放(若无其他引用)
$a = null变量存在,值为 null不影响引用关系数据变为 null,内存保留
$a = 0变量存在,值为 0不影响引用关系数据变为 0,内存保留

unset 在函数参数中的行为

php
<?php
declare(strict_types=1);

function testUnsetReference(string &$param): void
{
    $param = 'modified inside';
    unset($param); // 只断开函数内部 $param 的引用
    // 不影响外部变量
}

$external = 'original';
testUnsetReference($external);

echo $external; // 输出: modified inside
// 函数内先修改了引用,再 unset 了函数内的引用名
// 外部变量在 unset 之前已被修改

unset 与对象属性中的引用

php
<?php
declare(strict_types=1);

class Holder
{
    public string $value = 'initial';
}

$obj = new Holder();
$ref = &$obj->value;

$ref = 'changed via ref';
echo $obj->value; // 输出: changed via ref

unset($ref);
echo $obj->value; // 输出: changed via ref — 属性不受影响

实战示例

实战:安全地断开 foreach 引用

php
<?php
declare(strict_types=1);

class DataProcessor
{
    /**
     * 将数组中的所有字符串转为大写。
     */
    public function uppercaseAll(array &$items): void
    {
        foreach ($items as &$item) {
            if (is_string($item)) {
                $item = strtoupper($item);
            }
        }
        unset($item); // 关键:断开 foreach 引用
    }
}

$words = ['hello', 'world', 'php'];
$processor = new DataProcessor();
$processor->uppercaseAll($words);

print_r($words);
// Array ( [0] => HELLO [1] => WORLD [2] => PHP )

实战:引用计数管理

php
<?php
declare(strict_types=1);

class ResourceManager
{
    /** @var array<string, resource> */
    private array $handles = [];

    /**
     * 释放指定资源并断开所有引用。
     */
    public function release(string $key): void
    {
        if (isset($this->handles[$key])) {
            $handle = &$this->handles[$key];
            // 执行清理操作
            unset($handle);       // 断开局部引用
            unset($this->handles[$key]); // 从数组中移除
        }
    }

    public function set(string $key, mixed $value): void
    {
        $this->handles[$key] = $value;
    }

    public function has(string $key): bool
    {
        return isset($this->handles[$key]);
    }
}

$manager = new ResourceManager();
$manager->set('file', fopen('php://memory', 'r'));
$manager->release('file');
echo $manager->has('file') ? 'exists' : 'released'; // released

实战:临时引用模式

php
<?php
declare(strict_types=1);

/**
 * 使用临时引用处理数据,处理完毕后自动断开。
 */
function processWithTempRef(array &$data): void
{
    $temp = &$data['summary'];

    // 临时修改
    $temp = array_sum($data['values'] ?? []);

    // 使用完毕后断开
    unset($temp);
    // $data['summary'] 仍然存在,值已被修改
}

$report = ['values' => [10, 20, 30], 'summary' => 0];
processWithTempRef($report);

echo $report['summary']; // 输出: 60

实战:调试引用关系

php
<?php
declare(strict_types=1);

function dumpReferenceState(string $varName, mixed &$var): void
{
    echo "变量 {$varName}:\n";
    echo "  isset: " . (isset($var) ? 'true' : 'false') . "\n";
    echo "  value: " . var_export($var, true) . "\n";
    unset($var); // 仅断开函数内的引用
}

$a = 'test';
$b = &$a;

dumpReferenceState('a', $a);
dumpReferenceState('b', $b);

echo "外部 a: {$a}\n"; // test — 外部变量不受函数内 unset 影响
echo "外部 b: {$b}\n"; // test

注意事项

unset 不会触发析构

unset() 一个引用变量不会触发对象的析构函数,除非该变量是最后一个引用。

php
<?php
declare(strict_types=1);

class TrackedObject
{
    public function __construct()
    {
        echo "对象创建\n";
    }

    public function __destruct()
    {
        echo "对象销毁\n";
    }
}

$obj = new TrackedObject(); // 输出: 对象创建
$ref = &$obj;

unset($ref);
// 不输出 "对象销毁" — $obj 仍引用该对象

unset($obj);
// 输出: 对象销毁 — 最后一个引用被断开

unset 全局变量

在函数内部 unset() 全局变量的引用时,只断开局部引用,不影响全局变量。

php
<?php
declare(strict_types=1);

$globalVar = 'global';

function unsetGlobalRef(): void
{
    global $globalVar; // 创建对全局变量的引用
    unset($globalVar); // 只断开局部变量名,不影响全局变量
}

unsetGlobalRef();
echo $globalVar; // 输出: global — 全局变量仍然存在

unset 数组元素时的引用行为

php
<?php
declare(strict_types=1);

$data = ['a' => 1, 'b' => 2, 'c' => 3];
$ref = &$data['b'];

// unset 数组元素会同时断开所有引用
unset($data['b']);

echo isset($ref) ? 'exists' : 'not exists'; // not exists
echo isset($data['b']) ? 'exists' : 'not exists'; // not exists

最佳实践

  1. foreach 引用后务必 unset:这是最常见的最佳实践,避免循环变量泄漏导致意外的数组修改。
  2. 区分 unset 和赋 null:如果需要保留变量但清除其值,使用 $var = null;如果需要完全销毁变量,使用 unset()
  3. 局部引用及时断开:在函数内部创建的临时引用,使用完毕后应及时 unset,以明确意图。
  4. 理解对象生命周期unset 引用不会销毁对象,只有最后一个引用被 unset 时才会触发析构。
  5. 避免在循环中 unset 引用元素:遍历数组时 unset 引用元素可能导致意外行为,建议先收集要删除的键,遍历结束后统一删除。
php
<?php
declare(strict_types=1);

// 推荐:收集后再删除
$items = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4];
$keysToDelete = [];

foreach ($items as $key => &$value) {
    if ($value < 3) {
        $keysToDelete[] = $key;
    }
}
unset($value);

foreach ($keysToDelete as $key) {
    unset($items[$key]);
}

深入分析 unset 的内部行为

unset 与引用计数的精确关系

unset() 的本质操作是将变量从符号表中移除,并将对应 zvalrefcount 减 1。当 refcount 降到 0 时,PHP 才会真正释放内存。

php
<?php
declare(strict_types=1);

$a = 'hello';   // refcount=1
$b = $a;        // refcount=2 (COW)
unset($b);      // $b 从符号表移除,refcount=1
echo $a;        // 'hello' — $a 仍然存在

$c = &$a;       // refcount=2, is_ref=1
unset($c);      // $c 从符号表移除,refcount=1
echo $a;        // 'hello' — $a 仍然存在,值不变

unset 在函数参数中的作用

当函数参数通过引用传递时,在函数内部 unset 参数变量只会断开该局部引用,不会影响外部变量。

php
<?php
declare(strict_types=1);

function removeLocalRef(string &$param): void
{
    unset($param);  // 只断开 $param 对原始数据的引用
    // $param 在此之后是一个未定义的局部变量
}

$value = 'test';
removeLocalRef($value);
echo $value;  // 'test' — 外部变量不受影响

unset 对象属性的影响

unset() 可以移除对象的动态属性。但对于声明类型的属性(typed properties),unset() 后属性处于未初始化状态(PHP 8.0+),访问会抛出 Error

php
<?php
declare(strict_types=1);

class User
{
    public string $name;
    public int $age = 0;
}

$user = new User();
$user->name = 'Alice';
$user->age = 30;

unset($user->name);
// $user->name 现在是未初始化状态
// echo $user->name; // Error: Typed property User::$name must not be accessed before initialization

// 但可以重新赋值
$user->name = 'Bob';
echo $user->name; // 'Bob'

PHP 8.2+ 动态属性已弃用

PHP 8.2 中,在不支持动态属性的类上设置未声明的属性会发出弃用警告。PHP 9.0 中将成为错误。如需动态属性,请使用 #[AllowDynamicProperties] 注解。

常见误区与 FAQ

unset($a) 会让 $a 变成 null 吗?

不会。unset($a) 之后,$a 根本不存在了,它不再是 null——而是未定义。尝试访问未定义变量会产生 Warning(PHP 8.0+)。这与 $a = null 完全不同。

unset 在循环中的行为

foreach 循环中,使用引用时 unset 需要格外小心。不当使用可能导致意外的引用泄露。

php
<?php
declare(strict_types=1);

$data = ['a' => 1, 'b' => 2, 'c' => 3];
foreach ($data as &$value) {
    // ...
}
unset($value);  // 重要:手动断开最后的引用
// 否则 $value 会继续引用 $data['c'],
// 后续对 $value 的修改会影响 $data['c']

多个引用指向同一变量时 unset 一个

php
<?php
declare(strict_types=1);

$x = 'original';
$a = &$x;
$b = &$x;
// $a、$b、$x 三个变量名指向同一个 zval,refcount=3, is_ref=1

unset($a);
// $a 被移除,refcount=2
// $b 和 $x 仍然互相引用
echo $x;  // 'original'
echo $b;  // 'original'

unset($x);
// $x 被移除,refcount=1
// $b 仍然持有数据
echo $b;  // 'original'

参考链接