了解 PHP 8 扩展差异支持
差异概念是 OOP 的核心。差异(Variance)是一个总括术语,涵盖了各种子类型之间的相互关系。大约 20 年前,一对早期的计算机科学家 Wing 和 Liskov 设计了一个重要的定理,它是 OOP 子类型的核心,现在被称为 里氏替换原则。
在不涉及精确数学的情况下,这一原理可以被理解为以下内容:
如果可以用 X 的实例代替 Y 的实例,而应用程序的行为不会发生任何改变,那么类 X 就可以被视为类 Y 的子类型。
首次描述并提供利斯科夫替换原则精确数学公式定义的实际论文可在此处找到: A behavioral notion of subtyping,ACM Transactions on Programming Languages and Systems,作者 B. Liskov 和 J. Wing,1994 年 11 月 ( https://dl.acm.org/doi/10.1145/197320.197383 )。 |
在本节中,我们将研究 PHP 8 如何以 协变返回 和 逆变参数 的形式提供增强的差异(variance)支持。理解协变和逆变将提高你编写良好代码的能力。如果没有这种理解,你的代码可能会产生不一致的结果,并成为许多错误的根源。
让我们从协变返回开始。
了解协变 returns
PHP 中的协变支持旨在保持类型从最特殊到最一般的顺序。一个典型的例子就是尝试/捕获块(try/catch block)的设计:
-
在本例中,在 try 代码块中创建了一个 PDO 实例。下面的两个捕获块首先查找 PDOException。接下来的第二个捕获块会捕获任何实现 Throwable 的类。因为 PHP Exception 和 Error 类都实现了 Throwable,所以第二个捕获块最终会成为 PDOException 以外的任何错误的后备:
try { $pdo = new PDO($dsn, $usr, $pwd, $opts); } catch (PDOException $p) { error_log('Database Error: ' . $p->getMessage()); } catch (Throwable $t) { error_log('Unknown Error: ' . $t->getMessage()); }
-
在此示例中,如果 PDO 实例因参数无效而失败,错误日志中就会出现 "数据库错误" 条目,后面跟着从 PDOException 中获取的信息。
-
另一方面,如果发生了其他一般错误,错误日志中就会出现 "未知错误" 条目,然后是来自其他异常或错误类的信息。
-
但在本例中,捕捉块的顺序是相反的:
try { $pdo = new PDO($dsn, $usr, $pwd, $opts); } catch (Throwable $t) { error_log('Unknown Error: ' . $t->getMessage()); } catch (PDOException $p) { error_log('Database Error: ' . $p->getMessage()); }
-
由于 PHP 协变支持的工作方式,第二个捕获块永远不会被调用。相反,源于该代码块的所有错误日志条目都会以未知错误开头。
现在让我们看看 PHP 协变支持如何应用于对象方法的返回数据类型:
-
首先,我们定义了一个接口
FactoryIterface
,它标识了一个方法make()
。该方法接受一个数组作为参数,并返回一个ArrayObject
类型的对象:interface FactoryInterface { public function make(array $arr): ArrayObject; }
-
接下来,我们定义一个扩展
ArrayObject
的ArrTest
类:class ArrTest extends ArrayObject { const DEFAULT_TEST = 'This is a test'; }
-
ArrFactory
类实现了FactoryInterface
并完全定义了make()
方法。但请注意,该方法返回的是ArrTest
数据类型,而不是ArrayObject
:class ArrFactory implements FactoryInterface { protected array $data; public function make(array $data): ArrTest { $this->data = $data; return new ArrTest($this->data); } }
-
在程序调用代码块中,我们创建了
ArrFactory
实例,并运行其make()
方法两次,理论上产生了两个ArrTest
实例。然后,我们使用var_dump()
来显示生成的两个对象的当前状态:$factory = new ArrFactory(); $obj1 = $factory->make([1,2,3]); $obj2 = $factory->make(['A','B','C']); var_dump($obj1, $obj2);
-
在 PHP 7.1 中,由于不支持共变返回数据类型,因此会抛出一个致命错误。输出结果(如图所示)告诉我们,方法的返回类型声明与
FactoryInterface
中定义的不一致:root@php8_tips_php7 [ /repo/ch05 ]# php php8_variance_covariant.php PHP Fatal error: Declaration of ArrFactory::make(array $data): ArrTest must be compatible with FactoryInterface::make(array $arr): ArrayObject in /repo/ ch05/php8_variance_covariant.php on line 9
-
在 PHP 8 中运行同样的代码时,可以看到返回类型支持协变。如图所示,执行过程畅通无阻:
root@php8_tips_php8 [ /repo/ch05 ]# php php8_variance_covariant.php object(ArrTest)#2 (1) { ["storage":"ArrayObject":private]=> array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) } } object(ArrTest)#3 (1) { ["storage":"ArrayObject":private]=> array(3) { [0]=> string(1) "A" [1]=> string(1) "B" [2]=> string(1) "C" } }
ArrTest
扩展了 ArrayObject
,它是一个合适的子类型,完全符合 Liskov 替换原则所定义的标准。从最后的输出结果可以看出,PHP 8 比 PHP 早期版本更全面地遵循了真正的 OOP 原则。最终的结果是,在使用 PHP 8 时,你的代码和应用程序架构会更直观、逻辑更合理。
现在让我们来看看逆变参数。
使用逆变参数
协变涉及子类型从一般到特殊的排序,而逆变涉及相反的排序:从特殊到一般。在 PHP 7 和更早的版本中,还不能完全支持协变。因此,在 PHP 7 中,实现接口或扩展抽象类时,参数类型提示是 不变的。
另一方面,在 PHP 8 中,由于支持变量参数,您可以在顶层超级类和接口中自由地使用特定的参数。只要子类型是兼容的,就可以修改扩展类或实现类中的类型提示,使其更通用。
这样,在定义接口或抽象类时,您就可以更自由地定义整体架构。在 PHP 8 中,使用接口或超类的开发人员在实现子类逻辑时有了更大的灵活性。
让我们来看看 PHP 8 对逆变参数的支持是如何工作的:
-
在本例中,我们首先定义了一个
IterObj
类,它扩展了内置的ArrayIterator
PHP 类:// /repo/ch05/php8_variance_contravariant.php class IterObj extends ArrayIterator {}
-
然后,我们定义了一个抽象基类,该基类指定了一个方法
stringify()
。请注意,该方法唯一参数的数据类型是IterObj
:abstract class Base { public abstract function stringify(IterObj $it); }
-
接下来,我们定义了一个
IterTest
类,该类扩展了Base
并提供了stringify()
方法的实现。特别值得注意的是,我们重写了数据类型,将其改为iterable
:class IterTest extends Base { public function stringify(iterable $it) { return implode(',', iterator_to_array($it)) . "\n"; } } class IterObj extends ArrayIterator {}
-
接下来的几行代码将创建
IterTest
、IterObj
和ArrayIterator
的实例。然后,我们调用stringify()
方法两次,并将后面的每个对象作为参数:$test = new IterTest(); $objIt = new IterObj([1,2,3]); $arrIt = new ArrayIterator(['A','B','C']); echo $test->stringify($objIt); echo $test->stringify($arrIt);
-
在 PHP 7.1 中运行此代码示例会产生预期的致命错误,如图所示:
root@php8_tips_php7 [ /repo/ch05 ]# php php8_variance_contravariant.php PHP Fatal error: Declaration of IterTest::stringify(iterable $it) must be compatible with Base::stringify(IterObj $it) in /repo/ch05/php8_variance_ contravariant.php on line 11
由于 PHP 7.1 不支持逆变参数,因此它将参数的数据类型视为变量,并简单地显示一条信息,说明子类的数据类型与父类中指定的数据类型不兼容。
-
另一方面,PHP 8 提供了对逆变参数的支持。因此,它能识别
IterObj
(Base
类中指定的数据类型)是与iterable
兼容的子类型。此外,所提供的两个参数也都与iterable
兼容,从而允许程序继续执行。下面是 PHP 8 的输出结果:root@php8_tips_php8 [ /repo/ch05 ]# php php8_variance_ contravariant.php 1,2,3 A,B,C
PHP 8 支持 协变返回 和 逆变参数 的主要优点是不仅可以重写方法逻辑,还可以重写方法签名。您会发现,尽管 PHP 8 在执行良好的编码实践方面要严格得多,但增强的变量支持使您在设计继承结构时有了更大的自由度。从某种意义上说,至少在参数和返回值数据类型方面,PHP 8 的限制更少!
有关如何在 PHP 7.4 和 PHP 8 中应用差异支持的完整解释,请访问: https://wiki.php.net/rfc/covariantreturns-and-contravariant-parameters 。 |
现在我们来看看 SPL 的变化,以及这些变化在迁移到 PHP 8 后对应用程序性能的影响。