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
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
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
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
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
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
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
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
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
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
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()); // OKAPI 参数的灵活类型约束
<?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 类型并不是日常开发中频繁使用的特性。它主要适用于以下场景:
- 框架级别的类型约束:在框架的接口定义中,需要精确约束参数类型。
- 多接口组合:当参数需要同时满足多个接口,或者接受其他备选类型。
- 可空的多接口参数:
(Countable&Iterator)|null是最常见的 DNF 用法。
何时不用 DNF 类型
- 如果只需要简单的联合类型,使用
A|B即可。 - 如果只需要交集类型,使用
A&B即可。 - 如果参数类型过于复杂,考虑使用接口继承或 trait 来简化。
DNF 类型的性能影响
DNF 类型在编译时进行语法检查,在运行时进行类型验证。性能开销与其他类型声明相当,不会有额外负担。
最佳实践
保持 DNF 类型可读性:DNF 类型容易变得冗长,必要时添加注释解释约束的含义。
优先考虑接口设计:如果 DNF 类型过于复杂,可能意味着接口设计需要优化。考虑创建新的组合接口。
在框架代码中使用:DNF 类型更适合框架和库的公共 API,应用代码中应保持简单。
利用类型收窄简化逻辑:在函数体内尽早使用
instanceof或is_*函数进行类型收窄。注意 PHP 版本兼容性:DNF 类型需要 PHP 8.2+,如果项目需要兼容旧版本,使用 DocBlock 注释作为替代。
配合 PHPStan/Psalm 使用:静态分析工具可以更好地理解 DNF 类型约束,提供更精准的类型推断。