Skip to content

DNF 类型 (A&B)|C

概述

DNF 类型(Disjunctive Normal Form Types,析取范式类型)自 PHP 8.2 引入,允许将联合类型和交集类型组合使用。DNF 类型的基本语法是在联合类型中使用括号包裹交集类型部分,形如 (A&B)|C。这使得 PHP 的类型系统更加灵活,能够表达更复杂的类型约束。

核心要点

  • DNF 类型是联合类型和交集类型的组合,语法为 (A&B)|C
  • PHP 8.2 引入,解决了之前联合类型和交集类型不能混用的限制。
  • 括号用于包裹交集类型部分,表明这是一个"并"中的"且"。
  • DNF 类型主要用于方法参数接受特定实现或 null 等复杂场景。
  • :::

基础概念

什么是 DNF

DNF(Disjunctive Normal Form,析取范式)是布尔逻辑中的一种标准化形式。在类型系统中,DNF 类型指的是由多个"交集类型"通过"联合"组合而成的类型:

DNF = (交集1) | (交集2) | ... | (单一类型)

具体到 PHP 语法:

php
<?php
declare(strict_types=1);

// A 和 B 同时满足,或者 C 单独满足
function process((A&B)|C $param): void {}

// 更复杂的例子
function handle(
    (Countable&Iterator) | (Serializable&JsonSerializable) | array
    $input
): void {}

DNF 类型的组成

一个 DNF 类型声明由以下部分组成:

组成部分语法说明
交集部分(A&B)括号内使用 & 连接的类型
联合部分(A&B)|C括号外的 | 连接各分支
单一类型C不在括号内的普通类型
可空(A&B)|null交集类型与 null 的联合

语法与代码示例

基本 DNF 语法

php
<?php
declare(strict_types=1);

interface Cacheable { }
interface Loggable { }
interface Validatable { }

// 参数可以是:同时实现 Cacheable 和 Loggable 的对象,
// 或者同时实现 Validatable 和 Loggable 的对象
function process(
    (Cacheable&Loggable) | (Validatable&Loggable) $service
): void {
    // 在这里,$service 一定实现了 Loggable
    $context = $service->getLogContext();
    echo "Service context: " . json_encode($context) . PHP_EOL;
}

DNF 类型与 null 联合

最常见的 DNF 使用场景是接受一个同时实现多个接口的对象,或者接受 null:

php
<?php
declare(strict_types=1);

interface Countable { }
interface Iterator { }

// 接受同时实现 Countable 和 Iterator 的对象,或者 null
function analyze((Countable&Iterator)|null $collection): int
{
    if ($collection === null) {
        return 0;
    }

    return count($collection);
}

DNF 类型与具体类联合

php
<?php
declare(strict_types=1);

interface Renderable
{
    public function render(): string;
}

interface Cacheable
{
    public function getCacheKey(): string;
}

class SimpleComponent implements Renderable
{
    public function render(): string
    {
        return '<div>Simple</div>';
    }
}

class AdvancedComponent implements Renderable, Cacheable
{
    public function render(): string
    {
        return '<div>Advanced</div>';
    }

    public function getCacheKey(): string
    {
        return 'advanced:v1';
    }
}

// 接受 SimpleComponent 或同时实现 Renderable 和 Cacheable 的对象
function display(SimpleComponent|(Renderable&Cacheable) $component): void
{
    echo $component->render() . PHP_EOL;
}

display(new SimpleComponent());      // OK
display(new AdvancedComponent());    // OK(同时实现 Renderable 和 Cacheable)

多分支 DNF 类型

php
<?php
declare(strict_types=1);

interface JsonSerializable { }
interface ArrayAccess { }
interface Traversable { }

// 三种分支
function process(
    (JsonSerializable&ArrayAccess) |
    (Traversable&Countable) |
    array
$input): string {
    if (is_array($input)) {
        return 'array';
    }
    return 'object';
}

详细说明

PHP 8.2 之前与之后的对比

在 PHP 8.2 之前,联合类型和交集类型不能混用:

php
<?php
declare(strict_types=1);

// PHP 8.0/8.1:联合类型和交集类型不能混用
// function process((Countable&Iterator)|array $input): void {}
// Fatal Error: 语法错误

// PHP 8.2+:DNF 类型使其合法
function process((Countable&Iterator)|array $input): void {}

DNF 类型的编译时冗余检查

PHP 在编译时会检测 DNF 类型中的冗余:

php
<?php
declare(strict_types=1);

// 冗余:Countable&Countable 重复
function bad1((Countable&Countable)|array $x): void {}
// Fatal Error

// 冗余:如果 (Countable&Iterator) 已经是 array 的子集(假设)
// PHP 只能检测语法层面的冗余,不能检测语义层面的冗余

// 冗余:混合了更通用的类型
function bad2((Countable&Iterator)|Countable $x): void {}
// Fatal Error: Countable 比 Countable&Iterator 更通用,后者冗余

DNF 类型的类型收窄

在函数体内处理 DNF 类型参数时,需要使用 instanceof 进行类型收窄:

php
<?php
declare(strict_types=1);

interface Serializeable { public function serialize(): string; }
interface Cacheable { public function getCacheKey(): string; }

function handle((Serializeable&Cacheable)|string $input): string
{
    if (is_string($input)) {
        return $input;
    }

    // 此处 $input 同时是 Serializeable 和 Cacheable
    $key = $input->getCacheKey();
    return $input->serialize();
}

DNF 类型的限制

限制说明
交集类型必须在括号内(A&B)|C 合法,A&B|C 非法
交集类型内只能使用 &括号内不能使用 |
标量类型只能在括号外(int&string) 非法
self/parent/static 不能在交集内(self&Countable) 非法
php
<?php
declare(strict_types=1);

// 非法:交集类型不在括号内
function bad1(Countable&Iterator|array $x): void {}

// 非法:标量类型不能在交集中
function bad2((int&string)|bool $x): void {}

// 非法:self 不能在交集中
class MyClass
{
    function bad3((self&Countable)|null $x): void {}
}

实战示例

依赖注入中的 DNF 类型

php
<?php
declare(strict_types=1);

interface HasLogger
{
    public function getLogger(): LoggerInterface;
}

interface HasCache
{
    public function getCache(): CacheInterface;
}

interface HasDatabase
{
    public function getDatabase(): DatabaseInterface;
}

class ServiceContainer
{
    // 注入的服务必须同时有 Logger 和 Cache,或者同时有 Logger 和 Database
    public function register(
        (HasLogger&HasCache) | (HasLogger&HasDatabase) $service
    ): void {
        $logger = $service->getLogger();
        $logger->info("Service registered: " . get_class($service));
    }
}

class WebService implements HasLogger, HasCache
{
    public function getLogger(): LoggerInterface
    {
        return new FileLogger();
    }

    public function getCache(): CacheInterface
    {
        return new RedisCache();
    }
}

class DataProcessor implements HasLogger, HasDatabase
{
    public function getLogger(): LoggerInterface
    {
        return new FileLogger();
    }

    public function getDatabase(): DatabaseInterface
    {
        return new MySqlConnection();
    }
}

$container = new ServiceContainer();
$container->register(new WebService());       // OK
$container->register(new DataProcessor());    // OK

API 参数的灵活类型约束

php
<?php
declare(strict_types=1);

interface Arrayable
{
    public function toArray(): array;
}

interface Jsonable
{
    public function toJson(): string;
}

function sendData(
    (Arrayable&Jsonable) | string | array $payload
): void {
    if (is_string($payload)) {
        echo "Sending string: {$payload}" . PHP_EOL;
    } elseif (is_array($payload)) {
        echo "Sending array: " . json_encode($payload) . PHP_EOL;
    } else {
        echo "Sending object: " . $payload->toJson() . PHP_EOL;
    }
}

class ResponsePayload implements Arrayable, Jsonable
{
    public function __construct(private array $data) {}

    public function toArray(): array
    {
        return $this->data;
    }

    public function toJson(): string
    {
        return json_encode($this->data);
    }
}

sendData(new ResponsePayload(['key' => 'value']));
sendData('raw string');
sendData(['key' => 'value']);

注意事项

DNF 类型的适用场景

DNF 类型并不是日常开发中频繁使用的特性。它主要适用于以下场景:

  1. 框架级别的类型约束:在框架的接口定义中,需要精确约束参数类型。
  2. 多接口组合:当参数需要同时满足多个接口,或者接受其他备选类型。
  3. 可空的多接口参数(Countable&Iterator)|null 是最常见的 DNF 用法。

何时不用 DNF 类型

  • 如果只需要简单的联合类型,使用 A|B 即可。
  • 如果只需要交集类型,使用 A&B 即可。
  • 如果参数类型过于复杂,考虑使用接口继承或 trait 来简化。

DNF 类型的性能影响

DNF 类型在编译时进行语法检查,在运行时进行类型验证。性能开销与其他类型声明相当,不会有额外负担。

最佳实践

  1. 保持 DNF 类型可读性:DNF 类型容易变得冗长,必要时添加注释解释约束的含义。

  2. 优先考虑接口设计:如果 DNF 类型过于复杂,可能意味着接口设计需要优化。考虑创建新的组合接口。

  3. 在框架代码中使用:DNF 类型更适合框架和库的公共 API,应用代码中应保持简单。

  4. 利用类型收窄简化逻辑:在函数体内尽早使用 instanceofis_* 函数进行类型收窄。

  5. 注意 PHP 版本兼容性:DNF 类型需要 PHP 8.2+,如果项目需要兼容旧版本,使用 DocBlock 注释作为替代。

  6. 配合 PHPStan/Psalm 使用:静态分析工具可以更好地理解 DNF 类型约束,提供更精准的类型推断。

参考链接