编程

PHP 8.1: 相交类型(Intersection Types)

1044 2023-08-18 11:28:00

PHP 8.1 支持相交类型(Intersection Types),它允许为参数、属性或返回类型声明类型,并强制值属于所有声明的类/接口类型。这与允许任何已声明类型的联合类型(Union Type)相反。PHP 8.1的交集类型实现被称为“纯”交集类型,因为不允许在同一声明中组合联合类型和交集类型。

交集类型是通过使用 & 将类/接口名称组合来声明的。

作为一个用例示例,请考虑PHP内置的 IteratorCountable接口。Iterator 接口让使用 foreach迭代类对象成为可能。但是,除非同一个类实现 Countable接口,否则不可能对此类对象调用 count() 函数。

使用交集类型,现在可以键入检查类对象来实现 IteratorCountable 接口。

function count_and_iterate(Iterator&\Countable $value) {
    foreach($value as $val) {}
    count($value);
}

在上面的代码段中,$value 参数必须是同时实现 IteratorCountable 接口的类中的对象。传递任何其他值都会导致类型错误。

class CountableIterator implements \Iterator, \Countable {
    public function current(): mixed {}
    public function key(): mixed {}
    public function next(): void {}
    public function rewind(): void {}
    public function valid(): bool {}

    public function count(): int {}
}

由于 CountableIterator 类同时实现了 IteratorCountable 接口,因此此类的实例满足类型要求 Iterator&Countable

count_and_iterate(new CountableIterator());

但是,传递任何不同时实现 IteratorCountable 接口的类都会导致类型错误:

count_and_iterate(new stdClass());
Fatal error: Uncaught TypeError: count_and_iterate(): Argument #1 ($value) must be of type Iterator&Countable, stdClass given

纯交集类型

交集类型的初始实现只允许纯交集类型;不允许使用可为 nullable 或联合类型的复合类型,这会导致语法错误。

仅限类名及接口名

交集类型仅支持类和接口名称作为交集成员。不允许使用标量类型、arrayvoidmixedcallableneveriterablenull和其他类型。

function foo(string&int $val) {}

此代码段中的交集类型是不允许的,并且会在编译时导致致命错误。

Fatal error: Type string cannot be part of an intersection type in ... on line ...

此外,static, parent, 及 self 不能用在交集类型。

相交类型中的重复成员

相交点类型在编译时进行检查,而不会触发任何类自动加载。如果名称解析的类名在交集类型中重复,PHP会立即抛出致命错误。

但是,类别名和继承链不会在编译时解析,也不会导致冗余类/接口或类别名出现错误。

编译时检测重复成员

相交类型的每个成员只能使用一次。例如,Foo&Bar&Foo 是一个非法的类型声明,因为 Foo 类型被多次使用。

这样的错误在编译时被检测到,并导致致命错误。

function foo(Foo&Bar&Foo $val) {}
Fatal error: Duplicate type Foo is redundant in ... on line ...

冗余成员

当在交集类型中使用冗余类/接口时,php不会发出任何警告/通知,也不会抛出任何异常。

class A {}
class_alias(A::class, 'B');

function foo(A&B $val) {}

foo(new A());
foo(new B());

在上面的代码段中,交集类型 A&B 中的 B 类是多余的,因为 B 是类 A 的别名。这个代码段不会引起任何问题,因为PHP在编译时不会解析类别名。

静态分析器可能能够指出这种冗余的交叉点类型。

方差

交集类型的类型方差遵循 Liskov 替换原则,类似于联合类型和PHP的其他类型系统。

对于“交集类型”,“加宽”类型意味着向相交点添加新成员,而“缩小”类型则意味着删除相交点中的成员。

父类的参数类型可以在子类中扩展,允许逆变。对于交集类型,这意味着子类方法可以通过删除交集类型的成员来扩大其范围,从而放松范围。

class A {
    public function test(Foo&Bar $val) {}
}
class B extends A {
    public function test(Foo $val): Test&dsa {}
}

上面的代码段是有效的,因为 B::test 方法通过交集类型中的 Bar 成员有效地扩展了其 $val 参数。

返回类型协方差意味着子类的返回类型可以进一步缩小。在交集类型中,子类可以向交集添加成员,并且仍然履行其约定。

class A {
    public function test(): Foo {}
}
class B extends A {
    public function test(): Foo&Bar {}
}

B::test 方法的返回值被声明为交集类型 Foo&Bar。这是允许的,因为 B::test 的返回值继续满足 A::test 方法的返回类型。

属性类型是不变的,这意味着根本无法更改属性类型。允许更改属性类型声明中成员的顺序,但不允许添加或删除成员。

签名不匹配会导致编译时致命错误

class Foo {}
class Bar {}

class A {
    public function test(Foo $val) {}
}
class B extends A {
    public function test(Foo&Bar $val) {}
}
Fatal error: Declaration of B::test(Foo&Bar $val) must be compatible with A::test(Foo $val) in ... on line ...

此外,允许更改交集类型的单个成员,只要新成员是返回类型的协变,或者是参数类型的逆变。

Nullable 类型

在 PHP 7.1 引入 nullable 类型(例如 ?string)之前,有一种方法可以将参数类型隐式地声明为 nullable ,即设置默认值为 null

functiotest(string $test = null) {}

PHP 还支持将 null 传递给内部函数,即使它们没有被声明为 nullable 参数。PHP 8.1中对此进行了更改,对此类情况发出了弃用通知。

PHP 8.1中的交集类型不支持 nullable 类型语法(在PHP 7.1中引入),并会导致语法错误:

function test(?Foo&Bar $test) {}
Parse error: syntax error, unexpected token "&", expecting variable in ... on line ...

此外,隐式 nullable 语法也不允许,会导致编译时致命错误。

function test(Foo&Bar $test = null) {}
Fatal error: Cannot use null as default value for parameter $test of type Foo&Bar in ... on line ...

相关更新

  • PHP 8.0: 联合类型
  • PHP 8.2: 允许 nullfalse 作为标准独立类型
  • PHP 8.2: true 类型

向后兼容性影响

相交类型是 PHP 8.1 引入的新语法,使用 & 符号声明。

PHP 8.1 之前,任何使用相交类型的代码都会导致语法错误:

PHP Parse error: Syntax error, unexpected T_STRING, expecting T_VARIABLE on line ...

在旧的 PHP 版本中,交集类型不能 poly-filled 补丁兼容。然而,另一种方法是声明实现多个接口的新接口,并使类实现该接口:

interface CountableIterator implements \Iterator, \Countable {}

class CountableIteratorItem implements CountableIterator {
    // implement methods from both classes.
}
function count_and_iterate(CountableIterator $value) {
    foreach($value as $val) {}
    count($value);
}