看到一个很有意思的项目 https://github.com/php-defer/php-defer,这个项目只用了10
行左右的代码就实现了Go
语言中的defer
,看来灵活运用数据结构还是很重要的。项目源码如下:
<?php
function defer(?SplStack &$context, callable $callback): void {
$context ??= new class extends SplStack {
public function __destruct() {
while ($this->count() > 0) {
call_user_func($this->pop());
}
}
};
$context->push($callback);
}
略显抽象,怎么来理解它呢?$context
实际上就是一个栈变量,通过&
达到作用域内共用一个栈的效果,作用域内所有defer
的回调函数都放到$context
栈中。
关键点在于new class extends SplStack
一个匿名类继承并改写了SplStack
类的__destruct()
,使得$context
销毁时可以逆序调用入栈的回调函数,用法如下。
<?php
function foo(): void {
defer($_, function () {
echo "first defer\n";
});
defer($_, function () {
echo "second defer\n";
});
echo "before exception\n";
throw new Exception('My exception');
}
try {
foo();
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
相当于下面的Go
代码。
package main
import "fmt"
func foo() {
defer fmt.Println("first defer")
defer fmt.Println("second defer")
fmt.Println("before exception")
panic("My exception")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
foo()
}
调用结果如下。
$ php defer.php
before exception
second defer
first defer
My exception
跟我们设想的一样,defer
回调函数确实是逆序输出的,跟Go
语言的效果一致,可以再看看return
的。
<?php
function foo(): string {
defer($_, function () {
echo "first defer\n";
});
defer($_, function () {
echo "second defer\n";
});
return "foo";
}
echo foo(), "\n";
对应以下Go
代码。
package main
import "fmt"
func foo() string {
defer fmt.Println("first defer")
defer fmt.Println("second defer")
return "foo"
}
func main() {
fmt.Println(foo())
}
输出如下:
$ php defer.php
second defer
first defer
foo
无论是异常还是返回,该defer
函数的行为都跟Go
语言的defer
语句一致。
小小改造一下它,让它可以接收参数。
<?php
function defer(?SplStack &$context, callable $callback, ...$args): void {
$context ??= new class extends SplStack {
public function __destruct() {
while ($this->count() > 0) {
$item = $this->pop();
$func = array_shift($item);
if (empty($item)) {
call_user_func($func);
} else {
call_user_func($func, ...$item);
}
}
}
};
$context->push([$callback, ...$args]);
}
变化之处就在增加不限数量的参数...$args
,入栈时,以数组形式入栈,弹栈时,先获取数据的第一个元素,调用函数分为带参数和不带参数的区别,用法跟先前几乎一样。
<?php
defer($_, function ($item) {
printf("item: %s\n", $item);
}, "first");
defer($_, function ($item) {
printf("item: %s\n", $item);
}, "second");