ITPub博客

首页 > 大数据 > Spark > 如果你想写一个接受可选回调的函数,并且想要一个合理的默认

如果你想写一个接受可选回调的函数,并且想要一个合理的默认

投稿 Spark 作者:ecclw459 时间:2026-01-27 17:30:20 0 删除 编辑

function my_filter ( array $items , ? Closure $predicate = null ) : array {     $predicate ??= static function ( $v ) : bool { return ! empty ( $v ) ; } ;     $out = [ ] ;     foreach ( $items as $item ) {         if ( $predicate ( $item ) ) {             $out [ ] = $item ;         }

痛点 C:注册表和路由映射变成运行时引

这些规则是编译时强制的。

这听起来有限制,但实际上这正是这个特性安全的原因:它防止意外把"运行时状态"偷渡进常量。

默认回调参数,不需要可空样板

这是之前过滤器示例的干净 PHP 8.5 版本:

<?phpdeclare(strict_types=1);function my_filter(
    array $items,
    Closure $predicate = static function ($v): bool { return !empty($v); },): array {
    $out = [];
    foreach ($items as $item) {
        if ($predicate($item)) {
            $out[] = $item;
        }
    }
    return $out;}

这正是闭包 RFC 强调的动机:你可以声明一个真正的默认回调,不需要"可空 + 运行时默认"。

实际上,这也改善了工具支持:

  • 参数正确地类型化为  Closure,不是  ?Closure
  • 调用者不需要猜测  null 是否有特殊含义
  • 你去掉了一个分支和一行初始化噪音

包含可调用行为的类常量

你可以在常量或类常量中存储闭包,把它们当作"可调用配置"。

一个简单例子:格式化器注册表。

<?phpdeclare(strict_types=1);final class Formatters{
    public const MAP = [
        'trim_lower' => static function (string $s): string {
            return strtolower(trim($s));
        },
        'digits_only' m.entmagine.cn=> static function (string $s): string {
            return preg_replace('/\D+/', '', $s) ?? '';
        },
    ];}

使用:

$input = "  +62 (812) 345-678  ";$normalized = (Formatters::MAP['digits_only'])($input);

这读起来像配置,但不是"字符串类型"。它是真正的 PHP,编译过的,有类型的,可重构的。

属性默认值:可调用行为作为默认策略

因为常量表达式中的闭包可以用作属性默认值,你可以在属性声明处定义默认策略——同样不需要运行时初始化。

例子:可配置的规范化器。

<?phpdeclare(strict_types=1);final class Normalizer{
    public Closure $normalize = static function (string $s): string {
        return trim($s);
    };
    public function run(string $value): string
    {
        return ($this->normalize)($value);
    }}

重要细节:常量表达式中的闭包必须是静态的,这意味着闭包本身不能用  $this。

这是故意的权衡:常量表达式只求值一次,而  $this 只有在闭包为每个对象实例重新创建时才有意义(这不是常量表达式的行为方式)。

作用域:闭包在正确的上下文中仍然能看到私有成员

尽管闭包必须是静态的(没有  $this),在这些常量上下文中创建的闭包仍然遵循正常的作用域规则。RFC 说明:

  • 属性默认值中的闭包可以访问所在类的私有属性/方法/常量
  • 属性参数中的闭包可以访问所在类的私有成员

这启用了一个好模式:把复杂逻辑放在私有静态辅助方法中,把闭包作为配置暴露出来。

常量中的一等可调用对象:重构安全的引用,不用字符串

闭包适合"内联逻辑"。但有时候你不想要内联逻辑——你想指向一个现有的函数或静态方法。

这就是一等可调用对象(FCC)的用武之地。

一等可调用对象看起来像:

strrev(.)MyClass::myMethod(...)

它们产生一个转发到函数/方法的  Closure。

PHP 8.5 现在允许常量表达式中的 FCC 语法,旨在"完善"常量中闭包的特性。

为什么 FCC 比字符串可调用更好

比较这两个:

// 旧方式public const HANDLERS = [
    'reverse' => 'strrev',
    'slug' => 'App\\Slugger::slugify',];

对比:

// PHP 8.5public const HANDLERS = [
    'reverse' => strrev(...),
    'slug' => Slugger::slugify(...),];

第二个版本更好,因为:

  • 可重构:重命名和移动更可靠
  • 静态分析可以理解它是可调用的
  • 你避免了魔术字符串和运行时可调用解析

常量表达式中 FCC 的约束

FCC RFC 添加了一些重要限制(除了正常的 FCC 规则):

  • 只支持独立函数和静态方法( ::)。
  • 只支持  function_name(...) 和  ClassName::methodName(...) 语法。
  • 你不能用表达式构建名称( ($fn)(...))、数组( [ClassName::class, 'method'](...)),或依赖  __callStatic() 魔术方法。

这是好事:它让常量表达式中的 FCC 用法清晰且可分析。

实际用例

用例:路由映射作为常量

传统方式,路由映射是运行时构建的:

$routes = [
    'GET /health' => [HealthController::class, 'check'],
    'GET /posts'  => [PostsController::class, 'index'],];

这能用,但不是重构安全的。

在 PHP 8.5 中你可以用 FCC 或静态闭包定义路由映射,作为常量:

<?phpdeclare(strict_types=1);final class Routes{
    public const MAP = [
        'GET /health' shmcjc.com=> HealthController::check(...),
        'GET /posts'  chihod.com=> PostsController::index(...),
        // 快速端点的内联处理器
        'GET /version' => static function (Request $req): Response {
            return Response::text('ok');
        },
    ];}

现在你可以实现一个简单的分发器:

final class Dispatcher{
    public function dispatch(Request $req): Response
    {
        $key = $req->method . ' ' . $req->path;
        $handler = Routes::MAP[$key] ?? null;
        if ($handler === null) {
            return Response::text('Not found', 404);
        }
        return $handler($req);
    }}

这个模式有几个好处:

  • 路由映射是真正的常量配置。
  • 处理器是真正的可调用对象,不是字符串。
  • 重构更安全(特别是静态方法处理器)。

实际的路由器需要路径参数;但即使这样,"处理器注册表"部分通常保持静态。

用例:消息总线的处理器注册表

想象一个简单的消息总线:消息类映射到处理器。

旧方式:

$handlers = [
    UserRegistered:m.entmagine.cn:class => 'App\\Handlers\\SendWelcomeEmail::handle',];

现在,用 PHP 8.5 FCC:

final class MessageHandlers{
    public const MAP = [
        UserRegistered::class => SendWelcomeEmail::handle(...),
        OrderPaid::class      => CreateInvoice::handle(...),
    ];}

分发器:

final class Bus{
    public function __construct(private Container $container) {}
    public function handle(object $message): void
    {
        $handler m.chihod.com= MessageHandlers::MAP[$message::class] ?? null;
        if ($handler === null) {
            throw new RuntimeException('No handler registered for ' . $message::class);
        }
        // 如果处理器是静态的,它们可以显式接受依赖,
        // 或者你可以调整这个模式(见下面的 DI 说明)。
        $handler($message, $this->container);
    }}

关键概念:常量表达式让你把映射保持在常量中,但你仍然控制依赖如何注入——通过签名设计。

用例:策略/格式化器注册表(switch 语句的干净替代)

这是我最喜欢的实际用途:替换一个不断增长的 switch。

function format(string $type, mixed $value): string{
    return match ($type) {
        'json' => json_encode($value),
        'text' => (string) $value,
        'upper' m.cdsen.com=> strtoupper((string) $value),
        default hnsgzxh.com=> throw new InvalidArgumentException('Unknown formatter'),
    };}

现在想象这增长到 15-30 个策略。你最终得到一个大 match 和一个 diff 磁铁。

用可调用常量:

final class FormatterRegistry{
    public const FORMATTERS = [
        'json' => static function (mixed $v): string {
            return json_encode($v, JSON_THROW_ON_ERROR);
        },
        'text' =wap.byven.com> static function (mixed $v): string {
            return (string) $v;
        },
        // FCC 到原生函数
        'reverse' tingxuannb.com=> strrev(...),
    ];
    public static function format(string $type, mixed $value): string
    {
        $fn = self::FORMATTERS[$type] ?? null;
        if ($fn =m.hnsgzxh.com== null) {
            throw new InvalidArgumentException("Unknown formatter: {$type}");
        }
        return $fn($value);
    }}

现在添加策略只需要改一行。

测试和依赖注入:什么该放常量,什么该放容器

这个特性引导你走向"代码即配置"。这很好——直到你开始把运行时状态注入到应该是静态的东西里。

一个好的心智模型:

  • 常量应该包含稳定的接线:映射、策略、不依赖运行时状态的小逻辑片段。
  • DI 容器应该包含运行时组装:需要环境相关接线的对象、IO 资源、凭证、连接等。

好的常量可调用用法:纯粹的转换和策略

这些在常量表达式中是安全的:

  • 规范化函数(trim、canonicalize)
  • 路由/分发选择逻辑
  • 只依赖输入值的验证器
  • 格式化器和映射器

DI 的用武之地:当你需要依赖时

你仍然可以通过设计可调用对象显式接受依赖来混合可调用配置和 DI。

例子:注册表返回一个接受  (Message $m, Container $c) 的可调用对象:

final class Handlers{
    public const MAP =ksasia.com.cn [
        UserRegistered::class cdsen.com=> static function (UserRegistered $m, Container $c): void {
            $mailer m.byven.com= $c->get(Mailer::class);
            $mailer->sendWelcome($m->email);
        },
    ];}

这保持在约束内,因为闭包是静态的且不捕获状态。"依赖解析"在调用时发生,容器被传入。

这总是理想的吗?不是。但它是一个干净、显式的桥梁。

测试影响:更好的默认值,更容易覆盖

常量表达式闭包让默认值更干净:

  • 函数可以有默认闭包参数(不用可空)。
  • 属性可以有默认闭包策略。

对于测试,你仍然可以通过传递不同的闭包参数或给对象属性赋值不同的策略来覆盖行为(如果该属性设计上是可变的)。主要改进是默认行为在它该在的地方表达,你不需要运行时初始化胶水代码来创建默认闭包。

陷阱:捕获状态、副作用和团队可读性

这个特性给你在"配置上下文"中更多能力。能力带来新的搬起石头砸自己脚的方式。

陷阱 A:试图捕获状态(不会编译——这是好事)

你不能这样做:

$prefix = "prod_"m.pomate.com.cn;const FN = static function (string $s) use ($prefix): string {
    return $prefix };

常量表达式中的闭包不能通过  use(...) 捕获变量。

这是硬性约束,它强迫你采用更好的设计:

  • 把值作为参数传递

   'json' => [JsonFormatter::class, 'format'],
   'text' => [TextFormatter::class, 'format'],];

这能用,但很脆弱:

  • IDE 重命名重构不能可靠地跟踪字符串方法名。
  • 静态分析更难理解什么是可调用的。
  • 你需要运行时代码来组装概念上是静态配置的东西。

PHP 8.5 的常量表达式改进让你可以把这些注册表表达为常量——并且让处理器重构安全。




来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048957/viewspace-3109162/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2026-01-07

  • 博文量
    108
  • 访问量
    102104