为什么使用静态闭包?
在 PHP 中,我们越来越频繁地使用闭包(Closure)[1],例如依赖注入、中间件、集合回调,以及异步处理。
然而,闭包有一个可能出乎意料的行为:在实例方法中创建的任何闭包,都会自动携带对当前对象的引用,即使它根本没有使用$this。
这种行为可能会对对象的生命周期产生意外影响,如果不注意,还可能导致内存泄漏。
要理解其中的原因,我们首先需要了解 PHP 是如何管理内存的。
与 Java 等依赖垃圾回收器(延迟释放内存)的语言不同,PHP 使用的是引用计数(PHP 也有垃圾回收器[2]来处理循环引用,但那是另一个话题)。
当你赋值给一个变量时,其内容会存储在内存中;当变量不再被使用时,内存就可以被释放。例如:
<?php
$a = 'Hello';
$b = $a;PHP 不会为 b 再分配一块新的内存空间,而是让它指向与 a 相同的内存空间。如果你之后给 a 赋新值(如 "Hi"),则会分配新的内存空间,
如果再把 $b 赋值为 NULL,那么存放 "Hello" 的内存空间就不再被任何变量引用,可以被释放。PHP 通过维护引用计数来实现这一点,当计数降为 0 时,内存空间就被释放。
对于对象,当引用计数降为 0 时,在释放内存之前,如果类定义了 __destruct 方法,则会先调用它:
<?php
class Foo {
public function __construct() {
echo "Construct\n";
}
public function __destruct() {
echo "Destruct\n";
}
}
new Foo();
echo "End\n";输出:
Construct
Destruct
End对象没有被赋值给任何变量,因此构造函数调用后引用计数立即变为 0,__destruct 紧接着被调用。
而如果对象被赋值给变量,销毁就会被推迟:
<?php
$foo = new Foo();
echo "End\n";输出:
Construct
End
Destruct只要 $foo 还指向该对象,计数就保持为 1。只有在脚本结束、所有变量都被释放时才会销毁。要强制提前销毁,只需显式释放变量(重新赋值或使用 unset()):
<?php
$foo = new Foo();
echo "Before release\n";
$foo = null;
echo "After release\n";输出:
Construct
Before release
Destruct
After release下面来看一个定义了 getCallback() 方法的类 Bar,该方法返回一个读取 $this->id 属性的闭包:
<?php
class Bar {
public function __construct(private string $id) {
echo "Construct\n";
}
public function __destruct() {
echo "Destruct\n";
}
public function getCallback(): Closure {
return function(): string {
return $this->id;
};
}
}
$bar = new Bar('foo');
$getId = $bar->getCallback();
echo "Before releasing the object\n";
$bar = null;
echo "After releasing the object\n";
echo $getId() . "\n";
echo "End\n";输出:
Construct
Before releasing the object
After releasing the object
foo
End
Destruct当我们把 bar 赋值为 null 时,对象并没有被销毁,因为闭包访问了 this->id,从而构成了对对象的引用。只要闭包存在(即脚本结束前),引用计数就不会降为 0。如果我们之后把
如果闭包中没有使用$this,会发生什么?
<?php
class Bar {
public function __construct() {
echo "Construct\n";
}
public function __destruct() {
echo "Destruct\n";
}
public function getCallback(): Closure {
return function(): void {};
}
}
$bar = new Bar();
$callback = $bar->getCallback();
echo "Before releasing the object\n";
$bar = null;
echo "After releasing the object\n";
$callback = null;
echo "End\n";输出:
Construct
Before releasing the object
After releasing the object
Destruct
End对象仍然被保持存活!即使我们没有在闭包中使用 this,PHP 仍然会自动将 this 绑定到在实例方法中创建的任何闭包上——无论是否使用、是否为空。
闭包因此总是隐式地携带对对象的引用,这在阅读代码时是看不出来的。
当然,如果闭包是在静态方法中创建的,就不会有对 $this 的引用,对象会在变量释放时立即被销毁:
<?php
class Bar {
public function __construct() {
echo "Construct\n";
}
public function __destruct() {
echo "Destruct\n";
}
public static function getCallback(): Closure {
return function(): void {};
}
}
$bar = new Bar();
$closure = $bar::getCallback();
echo "Before releasing the object\n";
$bar = null;
echo "End\n";输出:
Construct
Before releasing the object
Destruct
End在闭包前加上 static 关键字,可以显式禁止它绑定到 $this。此时 PHP 不再存储任何对对象的引用(即使是隐式的)。
// ...
public function getCallback(): Closure {
return static function(): void {};
}
// ...输出:
Construct
Before releasing the object
Destruct
End如果需要在闭包中使用属性的值,可以通过 use 来传递:
// ...
public function getCallback(): Closure {
$id = $this->id;
return static function() use ($id): string {
return $id;
};
}这样,PHP 会在变量释放后立即销毁对象,因为闭包不再持有对它的引用。
如果你试图在静态闭包中使用 $this,PHP 会直接报错:
static function(): string {
return $this->id; // Error: Using $this when not in object context
};PHP 引擎通过这种方式保护你,避免意外捕获对象。
短闭包(fn() =>)提供了更简洁的语法,并且会自动捕获外部作用域的变量(无需 use)。
但它们在处理 $this 时与普通闭包行为一致:
public function getCallback(): Closure {
return fn(): string => $this->id;
}这里 $this 被隐式捕获,对象会一直存活到闭包被释放。
static 关键字同样适用于短闭包。外部变量仍会自动捕获,但 $this 不再被捕获:
public function getCallback(): Closure {
return static fn(): string => $this->id; // Error: Using $this when not in object context
}要想传递值而不捕获对象,只需提前提取出来:
public function getCallback(): Closure {
$id = $this->id;
return static fn(): string => $id;
}此时 id 是按值捕获的,this 不再参与其中……
[1] 闭包(Closure): https://www.php.net/closure
[2] 垃圾回收器: https://www.php.net/manual/en/features.gc.php