PHP 8.1: 相交类型(Intersection Types)
PHP 8.1 支持相交类型(Intersection Types),它允许为参数、属性或返回类型声明类型,并强制值属于所有声明的类/接口类型。这与允许任何已声明类型的联合类型(Union Type)相反。PHP 8.1的交集类型实现被称为“纯”交集类型,因为不允许在同一声明中组合联合类型和交集类型。
交集类型是通过使用 &
将类/接口名称组合来声明的。
作为一个用例示例,请考虑PHP内置的 Iterator
和 Countable
接口。Iterator
接口让使用 foreach
迭代类对象成为可能。但是,除非同一个类实现 Countable
接口,否则不可能对此类对象调用 count()
函数。
使用交集类型,现在可以键入检查类对象来实现 Iterator
和 Countable
接口。
function count_and_iterate(Iterator&\Countable $value) {
foreach($value as $val) {}
count($value);
}
在上面的代码段中,$value
参数必须是同时实现 Iterator
和 Countable
接口的类中的对象。传递任何其他值都会导致类型错误。
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
类同时实现了 Iterator
和 Countable
接口,因此此类的实例满足类型要求 Iterator&Countable
。
count_and_iterate(new CountableIterator());
但是,传递任何不同时实现 Iterator
和 Countable
接口的类都会导致类型错误:
count_and_iterate(new stdClass());
Fatal error: Uncaught TypeError: count_and_iterate(): Argument #1 ($value) must be of type Iterator&Countable, stdClass given
纯交集类型
交集类型的初始实现只允许纯交集类型;不允许使用可为 nullable 或联合类型的复合类型,这会导致语法错误。
仅限类名及接口名
交集类型仅支持类和接口名称作为交集成员。不允许使用标量类型、array
、void
、mixed
、callable
、never
、iterable
、null
和其他类型。
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: 允许
null
和false
作为标准独立类型 - 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);
}