PHP 8:类和枚举

  • 2022-12-27
    北京
  • 本文字数:14166 字

    阅读完需:约 46 分钟

本文属于专题文章《深入浅出 PHP 8》

根据w3tech的数据,PHP 仍是互联网上使用最为广泛的脚本语言之一,77.3%的网站在服务器端均使用该编程语言。PHP 8 为我们带来了许多新功能与优化,具体将在本系列文章中分析。

我们将在本文中总结 PHP 8 中类相关的新特性,包括:

  • 枚举,在类之上的一层,用于指定一个类型可能值的枚举列表。

  • 类属性新增只读修饰符,使属性在初始化后无法被修改。

  • 构造函数参数优化,可将构造函数参数值自动分配给一个对象属性。

只读类属性

在诸如数值对象的情况中,开发者们常常费尽脑汁让类的属性不可变。一般来说,在构造函数中初始化属性后,该属性理论上来说不能再被修改。另一种可行手段是将属性变为私有,且只声明一个公共 getter 方法。这种方法虽然减少了修改的空间,但仍不能排除变更的可能性。为使类属性不变,PHP 8.1 新增了对有类型的属性的只读支持,类型化的属性均可通过新的readonly关键词声明为只读。下面这段代码声明了int类型$a的只读属性,属性值仅在构造函数中设置一次。运行时脚本输出为1

<?phpclass A {   public readonly int $a;   public function __construct(int $a) {            $this->a = $a;   }}$a = new A(1);echo $a->a;
复制代码

可通过以下这段赋值语句将属性修改为只读:$a->a = 2;

该语句会导致如下报错:Fatal error: Uncaught Error: Cannot modify readonly property A::$a

只读readonly属性必须有类型,尝试用这段脚本将未类型化的属性变为只读:

<?phpclass A {   public readonly  $a;   public function __construct(int $a) {               $this->a = $a;   }}$a = new A(1);
复制代码

脚本报错信息如下:Fatal error: Readonly property A::$a must have type

如果你想不指定只读属性的类型,可以将其声明为混合型,如:public readonly mixed $a;

除类型限制外,readonly属性还有其他限制:只读的属性不能被声明为static,可参见下面这段脚本的演示:

<?phpclass A {   public static readonly int $a;   public function __construct(int $a) {               $this->a = $a;   }}$a = new A(1);
复制代码

脚本报错信息如下:Fatal error: Static property A::$a cannot be readonly

只读的属性只能在其被声明的作用域内被初始化。下面这段脚本在属性声明的作用域之外初始化了只读属性:

<?phpclass A {   public  readonly int $a;}$a = new A();$a->a=1; 
复制代码

运行时脚本报错信息如下:Fatal error: Uncaught Error: Cannot initialize readonly property A::$a from global scope

虽然也可以在初始化时为只读属性分配默认值,但这么做用处不大,因为类常量也能做到。因此,为只读属性设置一个默认值不被允许。下面这段脚本试图为一个只读属性声明默认值:

<?phpclass A {   public  readonly int $a=1;   public function __construct(int $a) {               $this->a = $a;   }}
复制代码

运行时脚本报错信息如下:Fatal error: Readonly property A::$a cannot have default value

只读属性这一功能的目的,是使一个类属性不可变。因此,只读属性在初始化之后无法通过 unset()重置。下面这段脚本尝试在初始化一个只读属性后对其调用 unset():。

<?phpclass A {   public  readonly int $a;      public function __construct(int $a) {               $this->a = $a;       unset($this->a);   }}$a = new A(1);
复制代码

运行时脚本报错信息如下:Fatal error: Uncaught Error: Cannot unset readonly property A::$a

但在初始化之前却可以对只读属性调用 unset(),比如下面这段脚本:

<?phpclass A {   public  readonly int $a;        public function __construct(int $a) {        unset($this->a);       $this->a = $a;           }} $a = new A(1);echo $a->a;
复制代码

脚本运行成功,输出值为1

简单的重新赋值或其他运算符操作都无法修改readonly属性。下面这段脚本没有通过重新赋值语句修改只读属性,而是试图对其使用增量运算符:

<?phpclass A {   public  readonly int $a;        public function __construct(int $a) {                $this->a = $a;          }} $a = new A(1);$a->a++;
复制代码

目的一样,因此错误信息也是一样的:Fatal error: Uncaught Error: Cannot modify readonly property A::$a

实际上,不变的仅仅是readonly的属性,不影响其中存储的对象或资源。我们可以修改任何存储在readonly属性中对象及非只读属性。这段脚本试图通过一个类型为object的只读属性$obj,设置类属性$a的数值。

<?phpclass A {   public int $a;    public function __construct(public readonly object $obj) {}}$a = new A(new stdClass); $a->obj->a=1; echo $a->obj->a;
复制代码

脚本运行成功,输出为1

PHP 8.2 中将新增的只读类作为对先前只读类属性功能的扩展。如果一个类使用readonly修饰符声明,那么其中所有的类属性均默认为只读。只读类的类属性必须有类型且非静态,以下面这段脚本为例:

readonly class A{    public int $a;    public string $b;    public array $c;        public function __construct() {        $this->a = 1;        $this->b = "hello";        $this->c = [                    "1" => "one",                    "2" => "two",                   ];    }}
复制代码

只读类的确有一些限制;它不能定义动态属性,只有只读类才能扩展另一个只读类。

构造函数属性提升

PHP 8.0 中新引入的构造函数属性提升功能取代了类属性声明和初始化。具体请见下面这段脚本,类属性$pt1$pt2$pt3$pt4均在 Rectangle 类中声明,并在类的构造函数中初始化。

<?phpclass Point {    public function __construct(        public float $pt = 0.0,    ) {}}class Rectangle {  public Point $pt1;  public Point $pt2;  public Point $pt3;   public Point $pt4;  public function __construct(        Point $pt1,        Point $pt2,        Point $pt3,        Point $pt4,    ) {        $this->pt1 = $pt1;        $this->pt2 = $pt2;        $this->pt3 = $pt3;        $this->pt4 = $pt4;          }}
复制代码

新增构造函数属性提升后,脚本会缩减到以下内容:

<?php class Point {    public function __construct(        public float $pt = 0.0,    ) {}}class Rectangle {   public function __construct(       public Point $pt1,       public Point $pt2,       public Point $pt3,       public Point $pt4,    ) {           }}
复制代码

构造函数本身可以为空,在其中的语句会在构造函数入参提升至对应类属性后运行。构造函数提升至类属性所要满足的唯一条件是包含有一个可见性修改器。构造函数的参数值会自动赋给同名的类属性。

下面这段使用 Rectangle 类的类构造函数调用的脚本示例,展示了如何自动提升并初始化公共构造函数参数至类属性:

<?php $pt1=new Point(); $pt2=new Point(); $pt3=new Point(); $pt4=new Point(); $a = new Rectangle($pt1,$pt2,$pt3,$pt4);// 类属性值输出为:var_dump($a->pt1);var_dump($a->pt2);var_dump($a->pt3);var_dump($a->pt4);
复制代码

可以看出,脚本的确添加了类属性且被隐式初始化,其输出如下:

object(Point)#1 (1) { ["pt"]=> float(0) } object(Point)#2 (1) { ["pt"]=> float(0) } object(Point)#3 (1) { ["pt"]=> float(0) } object(Point)#4 (1) { ["pt"]=> float(0) }
复制代码

如果构造函数参数不包含可见性修改器,则不会被正确提升至对应类属性。但并不是所有构造函数参数都需要被提升。以下这段脚本中参数$pt4没有声明可见性修饰符,因此也没有构造器参数提升。

<?php class Point {    public function __construct(        public float $pt = 0.0,    ) {}}class Rectangle {   public function __construct(       public Point $pt1,       public Point $pt2,       public Point $pt3,       Point $pt4,    ) {         }}
复制代码

同理,调用 Rectangle 类构造器并输出类属性值。但这个例子中因为$pt4没有可见性修饰器,没有被提升至对应类属性,所以结果不尽相同。我们会得到一个警告信息:Warning: Undefined property: Rectangle::$pt4

需要像下面这段脚本中一样,明确声明并初始化$pt4类属性:

<?php class Point {    public function __construct(        public float $pt = 0.0,    ) {}}class Rectangle {   public Point $pt4;  public function __construct(       public Point $pt1,       public Point $pt2,       public Point $pt3,       Point $pt4,    ) {       $this->pt4=$pt4;  }}
复制代码

这样,我们就可以和之前一样调用构造函数,并得到一样的类属性输出结果。

类构造函数参数被提升至对应类属性还需要满足的另一个条件是,构造函数不能是可调用类型。下面这段脚本声明了一个可调用类型的构造函数参数:

<?phpclass Rectangle {     public function __construct(       public callable $pt1,       public callable $pt2,       public callable $pt3,       public callable $pt4,    ) {          }}
复制代码

脚本运行时会生成一条错误信息:Fatal error: Property Rectangle::$pt1 cannot have type callable

在 PHP 8 系列的首篇文章中,我们分析了如何在初始化器中使用 new 的操作符,以及如何为函数参数初始化默认值。new 操作符同样可被用于设置构造器参数的默认值以及构造器属性提升,如下面这段脚本所示:

<?phpclass Point {    public function __construct(        public float $pt = 0.0,    ) {}}class Rectangle {  public function __construct(       public Point $pt1=new Point(),       public Point $pt2=new Point(),       public Point $pt3=new Point(),       public Point $pt4=new Point(),    ) {           }}
复制代码

这个Rectangle类的构造函数可以在没有任何构造函数入参的情况下被调用,并输出提升后的类属性值:

$a = new Rectangle();var_dump($a->pt1);var_dump($a->pt2);var_dump($a->pt3);var_dump($a->pt4);
复制代码

输出为:

object(Point)#2 (1) { ["pt"]=> float(0) } object(Point)#3 (1) { ["pt"]=> float(0) } object(Point)#4 (1) { ["pt"]=> float(0) } object(Point)#5 (1) { ["pt"]=> float(0) }
复制代码

在 define()中使用对象

内置define()函数可用于定义命名常量。随着 PHP 8.1 的出现,对象也可以传入define(),如下面这段脚本所示:

<?phpclass Point {    public function __construct(        public float $pt = 0.0,    ) {}} define("PT1", new Point(1.0));var_dump(PT1); 
复制代码

脚本输出为:object(Point)#1 (1) { ["pt"]=> float(1) }

类常量可被声明为 final

PHP 8.1 中,允许使用 final 关键字声明类常量。除此之外,如果一个类常量在类中被声明为 final,那么任何其的扩展类都不能覆盖或重新定义该常量的数值。在下面这段脚本中,类常量 c 在类 A 中被声明为 final,但又在扩展类 A 的类 B 中被重新定义:

<?phpclass A{    final public const c = 1;}class B extends A{    public const c = 2;}
复制代码

脚本运行后会出现以下报错信息:Fatal error: B::c cannot override final constant A::c

特殊的::class 常量可用于对象

从 PHP 8.0 开始,特殊的::class常量不仅允许在编译时进行绝对类名的解析,也可用于类对象。区别在于类名解析出现在运行时的对象中,而类则用于编译时解析。将::class用于对象等同于对对象调用get_class()。下面这段脚本对类 A 中的一个对象使用了::class,输出为“A”:

<?php class A {} $a = new A();print $a::class;?>
复制代码

接口常量可被覆盖

从 PHP 8.1 开始,接口常量可以被继承的类或接口覆盖。在下面这段脚本中,接口常量 c 被同名的类常量覆盖,用于覆盖的常量值可以相同也可以不同。

<?phpinterface A{    public const c = 1;}class B implements A{    public const c = 2;   public function __construct() {            }}
复制代码

两个常量值均可被输出:

echo A::c;echo B::c;
复制代码

输出为:

12
复制代码

但如果接口常量被声明为 final 则不能被覆盖,这一点与被声明为 final 的类常量同理。下面这段脚本试图覆盖一个被声明为 final 的接口常量:

<?phpinterface A{    final public const c = 1;}class B implements A{    public const c = 2;}
复制代码

脚本运行会输出下面这段错误信息:Fatal error: B::c cannot override final constant A::c

自动加载函数__autoload()被移除

在 PHP 7.2.0 中被废弃的__autoload()函数在 PHP 8.0 中被移除。如果调用__autoload()则会输出下面这段错误信息:Fatal error: Uncaught Error: Call to undefined function __autoload()

枚举

缩写为 enum 的枚举,是用于声明一个具有明确可能值的一个新增自定义类型功能。语言结构enum可用于声明一个枚举,最简单的枚举可以为空:

enum FirstEnum {}
复制代码

一个enum可以通过case关键字声明可能值,比如:

enum SortType {  case Asc;  case Desc;  case Shuffle;}
复制代码

因为枚举与类这二者的相似性,相关讲解会一同进行。

为什么说枚举与类相似

  • 枚举即为类。示例中的枚举SortType是一个类,其中的可能值均为类的对象实例。

  • 枚举与类、接口、特征共用相同的命名空间。

  • 枚举与类均可自动加载。

  • 每个枚举值,如SortType枚举中的AscDescShuffle均为对象实例。枚举值的实例可以通过object的类型检查。

  • 枚举值或 case 名称在内部表示为类常量,因此区分大小写。

枚举在诸如以下几类的场景中非常有用:

  • 对系列常量的结构化替代;

  • 数据建模;

  • 单体式编程;

  • 定义领域模型;

  • 不支持的数值无法显示,可减少代码测试的验证工作。

下面我们会配合示例对枚举进行探讨。枚举值本身是对象,因此对象能用的地方枚举值也能用,其中就包括函数的参数类型和返回类型。在下面这段脚本中,函数的参数类型和返回类型均为枚举SortType

<?phpenum SortType  {   case Asc;  case Desc;  case Shuffle; }class A {  public function sortType():SortType{    return SortType::Asc;  } }$a=new A();var_dump($a->sortType());
复制代码

脚本的输出为:enum(SortType::Asc)

和系列中的首篇文章一样,我们还是使用数组排序的例子。sortArray函数的类型是SortType枚举类型。

function sortArray(SortType $sortType) {   $arrayToSort=array("B", "A", "f", "C");}
复制代码

枚举对象的数值可用==操作符比对:if ($sortType == SortType::Asc){...}

同样的例子但使用枚举,效果如下:

<?phpenum SortType {  case Asc;  case Desc;  case Shuffle;   } function sortArray(SortType $sortType) { $arrayToSort=array("B", "A", "f", "C");    if ($sortType == SortType::Asc) {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }          } elseif ($sortType == SortType::Desc) {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }          } elseif ($sortType == SortType::Shuffle){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }       }}$val = SortType::Asc;sortArray($val);sortArray(SortType::Desc);sortArray(SortType::Shuffle); 
复制代码

我们的数组排序示例脚本的输出如下:

0 = A 1 = B 2 = C 3 = f 0 = f 1 = C 2 = B 3 = A 0 = f 1 = A 2 = B 3 = C
复制代码

因为枚举例或者说可能值是一个对象实例,因此需要配合instanceof操作符与枚举值一起使用,如:if ($sortType instanceof SortType) {...}

枚举值不会被转换为字符串,因此不能当作字符串使用。也就是说,如果直接用字符串入参调用sortArray()函数:sortArray('Shuffle');

就会导致报错:Fatal error: Uncaught TypeError: sortArray(): Argument #1 ($sortType) must be of type SortType, string given

所有枚举值或例均有一个 name 只读属性,该属性值为枚举例的名称,区分大小写。name 属性可用于调试,如用这段打印语句输出“Asc”:print SortType::Asc->name;

枚举值不可重复,且区分大小写。下面这段脚本中的枚举值满足这一要求:

<?phpenum SortType  {   case Asc;  case Desc;  case ASC; }
复制代码

但这段脚本中的枚举值有重复:

<?phpenum SortType  {  case Asc;  case Desc;  case Asc;}
复制代码

脚本会输出以下错误信息:Fatal error: Cannot redefine class constant SortType::Asc我们在此讨论的均为基本枚举或者说纯粹枚举,纯粹枚举只会定义没有关联数据的纯粹枚举值。下面我们要讨论的另一种枚举类型被称作是回退枚举。

回退枚举

回退枚举的枚举值定义了字符串类型或int类型的标量替代,如:

enum SortType:int {  case Asc=1;  case Desc=2;  case Shuffle=3;}
复制代码

标量替代可以是int类型或字符串类型,但不能是二者联合的int|string,所有回退枚举值必须声明一个标量值,以这段回退枚举脚本为例:

<?phpenum SortType:int {  case Asc=1;  case Desc=2;  case Shuffle;}
复制代码

会生成以下错误信息:Fatal error: Case Shuffle of backed enum SortType must have a value

回退枚举的标量替代不能重复。下面这段脚本试图为两个枚举值声明同样的标量替代:

<?phpenum SortType:int {  case Asc=1;  case Desc=2;  case Shuffle=2;}
复制代码

脚本输出的错误信息如下:Fatal error: Duplicate value in enum SortType for cases Desc and Shuffle

标量替代可以为字面值,也可以为字面表达式,如:

<?phpenum SortType:int {  case Asc=1;  case Desc=2+1;  case Shuffle=3-3;}
复制代码

所有回退枚举值或回退枚举用例都有一个只读属性value,其属性值为回退枚举值的标量值。以下面这段脚本为例,打印语句会输出 Desc 枚举值的标量替代值:print SortType::Desc->value;

这里的 value 是只读属性且不可修改。下面这段脚本试图给回退枚举值的 value 属性赋值为一个变量的引用:

$sortType = SortType::Desc;$ref = &$sortType->value;
复制代码

这段赋值语句会输出以下错误信息:Fatal error: Uncaught Error: Cannot modify readonly property SortType::$value

回退枚举实现了一个内部接口BackedEnum,其中包含两个方法:

  • from(int|string): self:以一个回退枚举用例的标量枚举值为入参,返回对应的枚举用例。如果标量值没有对应用例则返回ValueError

  • tryFrom(int|string): ?self:以回退枚举用例的标量枚举值为入参,输出对应枚举用例。如果标量值没有对应用例则返回空。

下面这段脚本展示了这两种方法的使用场景:

<?php enum SortType:int {  case Asc=1;  case Desc=2;  case Shuffle=3;}$sortType =  SortType::from(1); print $sortType->value; echo “<br/>”;$sortType = SortType::tryFrom(4) ?? SortType::Desc;print $sortType->value;  echo “<br/>”;$sortType =  SortType::from("4");
复制代码

输出为:

12Fatal error: Uncaught ValueError: 4 is not a valid backing value for enum "SortType"
复制代码

方法from()tryFrom()使用严格和弱类型模式,默认为弱类型模式,也就意味着隐式转换。如下面这段脚本所示,用于 integer 的浮点和字符串值均会被转换为整数值:

<?phpenum SortType:int {  case Asc=1;  case Desc=2;  case Shuffle=3;}$sortType =  SortType::from(1.0); print $sortType->value; echo "<br/>";$sortType = SortType::tryFrom("4") ?? SortType::Desc;print $sortType->value;  echo "<br/>";$sortType =  SortType::from("2.0");print $sortType->value; 
复制代码

输出为:

122
复制代码

如果期望类型为 int,那么传入的字符串必须要能够转换为 integer,否则:$sortType = SortType::from("A");这段脚本会生成下面这条错误信息:Fatal error: Uncaught TypeError: SortType::from(): Argument #1 ($value) must be of type int, string given在严格类型模式中,无法使用这种类型转换,否则会输出以下错误信息:Fatal error: Uncaught TypeError: SortType::from(): Argument #1 ($value) must be of type int, float given

纯粹枚举和回退枚举均有一个内部接口的UniEnum实现,提供静态方法cases(),可输出枚举的可能值或枚举情况。下面这段脚本演示了cases()的使用方法:

<?php   enum SortType  {  case Asc;  case Desc;  case Shuffle;   } enum BackedSortType:int {  case Asc=1;  case Desc=2;  case Shuffle=3;}var_dump(SortType::cases());var_dump(BackedSortType::cases());
复制代码

输出为:

array(3) { [0]=> enum(SortType::Asc) [1]=> enum(SortType::Desc) [2]=> enum(SortType::Shuffle) } array(3) { [0]=> enum(BackedSortType::Asc) [1]=> enum(BackedSortType::Desc) [2]=> enum(BackedSortType::Shuffle) }
复制代码

枚举可包含方法也可实现接口

无论纯粹枚举还是回退枚举,均可声明方法,这点与类的方法实例类似。枚举同样也可以实现接口,但必须在函数之外的实现接口函数。下面这段脚本类似先前的数组排序例子,但包含一个实现了接口的枚举。该枚举除了实现接口函数外,还额外实现了一个不属于该接口的函数:

<?phpinterface SortType{    public function sortType(): string;}enum SortTypeEnum implements SortType   {  case Asc;  case Desc;  case Shuffle;   public function sortType(): string    {        return match($this) {            SortTypeEnum::Asc => 'Asc',            SortTypeEnum::Desc => 'Desc',            SortTypeEnum::Shuffle => 'Shuffle',        };    }   public function notFromInterface(): string    {        return "Function Not From Interface";    }} function sortArray(SortType $sortType) { $arrayToSort=array("B", "A", "f", "C");if ($sortType->sortType() == "Asc") {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->sortType() == "Desc") {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->sortType() == "Shuffle"){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";          }         elseif  ($sortType instanceof SortType){                           sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }   }}$val = SortTypeEnum::Asc; sortArray(SortTypeEnum::Asc);sortArray(SortTypeEnum::Desc);sortArray(SortTypeEnum::Shuffle);print SortTypeEnum::Asc->notFromInterface(); 
复制代码

这段脚本的输出为:

0 = A 1 = B 2 = C 3 = f0 = f 1 = C 2 = B 3 = A0 = C 1 = f 2 = B 3 = AFunction Not From Interface 
复制代码

回退枚举同样也可以实现接口并提供额外的方法,如下面这段脚本所示:

<?phpinterface SortType{    public function sortType(): string;}enum SortTypeEnum: string implements SortType{  case Asc = 'A';  case Desc = 'D';  case Shuffle = 'S';    public function sortType(): string    {        return match($this->value) {            'A' => 'Asc',            'D' => 'Desc',            'S' => 'Shuffle',        };    }   public function notFromInterface(): string    {        return "Function Not From Interface";    }} function sortArray(SortType $sortType) { $arrayToSort=array("B", "A", "f", "C");    if ($sortType->sortType() == "Asc") {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->sortType() == "Desc") {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->sortType() == "Shuffle"){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";          }         elseif  ($sortType instanceof SortType){                           sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }      }}  sortArray(SortTypeEnum::Asc);sortArray(SortTypeEnum::Desc);sortArray(SortTypeEnum::Shuffle);print SortTypeEnum::Asc->notFromInterface(); 
复制代码

输出为:

0 = A 1 = B 2 = C 3 = f0 = f 1 = C 2 = B 3 = A0 = C 1 = f 2 = B 3 = AFunction Not From Interface 
复制代码

枚举可声明静态方法

枚举也可以声明静态方法。继续使用上面的数组排序例子,我们在其中声明了一个静态方法chooseSortType(),该方法可依据要排序的数组长度选择排序类型:

<?phpenum SortType {   case Asc;  case Desc;  case Shuffle;   public static function chooseSortType(int $arraySize): static    {        return match(true) {            $arraySize < 10 => static::Asc,            $arraySize < 20 => static::Desc,            default => static::Shuffle,        };    }} function sortArray(array $arrayToSort) {      if (SortType::chooseSortType(count($arrayToSort)) == SortType::Asc) {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif (SortType::chooseSortType(count($arrayToSort)) == SortType::Desc) {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif (SortType::chooseSortType(count($arrayToSort)) == SortType::Shuffle){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        }          }  $arrayToSort=array("B", "A", "f", "C");sortArray($arrayToSort); $arrayToSort=array("B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C");sortArray($arrayToSort);$arrayToSort=array("B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C");sortArray($arrayToSort);
复制代码

输出为:

0 = A 1 = B 2 = C 3 = f0 = f 1 = f 2 = f 3 = C 4 = C 5 = C 6 = B 7 = B 8 = B 9 = A 10 = A 11 = A0 = A 1 = B 2 = B 3 = C 4 = B 5 = C 6 = f 7 = A 8 = f 9 = C 10 = B 11 = f 12 = f 13 = A 14 = A 15 = B 16 = C 17 = f 18 = A 19 = B 20 = C 21 = f 22 = C 23 = A 
复制代码

枚举可声明常量

枚举同样可声明常量。下面这段脚本中声明了一个常量 A:

<?phpenum SortType  {   case Asc;  case Desc;  case Shuffle;    public  const A = 1; }
复制代码

声明的常量可以引用枚举本身的场景,这些场景也回会是常量。下面这段数组排序脚本中使用的是在枚举中声明的常量:

<?phpenum SortType {   case Asc;  case Desc;  case Shuffle;   public const ASCENDING = self::Asc;   public const DESCENDING = self::Desc;   public const SHUFFLE = self::Shuffle;} function sortArray(SortType $sortType) {     $arrayToSort=array("B", "A", "f", "C");    if ($sortType == SortType::Asc) {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType == SortType::Desc) {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType == SortType::Shuffle){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }      }}sortArray(SortType::ASCENDING); sortArray(SortType::DESCENDING); sortArray(SortType::SHUFFLE); 
复制代码

输出为:

0 = A 1 = B 2 = C 3 = f0 = f 1 = C 2 = B 3 = A0 = C 1 = B 2 = f 3 = A
复制代码

因为枚举值本身是常量,因此明确的常量不能重新定义一个枚举的值,比如下面这段脚本中所演示的:

<?phpenum SortType:int {  public const Asc = "Asc";  case Asc=1+1;  case Desc=2;  case Shuffle=3; }
复制代码

脚本会输出以下这段错误信息:

Fatal error: Cannot redefine class constant SortType::Asc

枚举值必须在编译时可评估,如下面这段脚本中的枚举常量所示:

<?php enum SortType:int {  const CONSTANT=4;  case Asc=1;  case Desc=2;  case Shuffle=CONSTANT;}
复制代码

会输出以下错误信息:Fatal error: Enum case value must be compile-time evaluatable

使用特质的枚举

枚举可以使用特质(trait)。下面这段数组排序脚本声明了一个ChooseSortType特质,并在枚举中使用了该特质:

<?phptrait ChooseSortType {     public function chooseSortType(int $arraySize): SortType    {        return match(true) {            $arraySize < 10 => SortType::Asc,            $arraySize < 20 => SortType::Desc,            default => SortType::Shuffle,        };    }}enum SortType {   use ChooseSortType;  case Asc;  case Desc;  case Shuffle;} function sortArray(SortType $sortType, array $arrayToSort) {      if ($sortType->chooseSortType(count($arrayToSort)) == SortType::Asc) {             sort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->chooseSortType(count($arrayToSort)) == SortType::Desc) {             rsort($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        } elseif ($sortType->chooseSortType(count($arrayToSort)) == SortType::Shuffle){                           shuffle($arrayToSort);             foreach ($arrayToSort as $key => $val) {                echo "$key = $val ";             }  echo "<br/>";        }          }  $arrayToSort=array("B", "A", "f", "C");sortArray(SortType::Desc,$arrayToSort); $arrayToSort=array("B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C");sortArray(SortType::Asc,$arrayToSort);$arrayToSort=array("B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C","B", "A", "f", "C");sortArray(SortType::Desc,$arrayToSort);
复制代码

输出为:

0 = A 1 = B 2 = C 3 = f0 = f 1 = f 2 = f 3 = C 4 = C 5 = C 6 = B 7 = B 8 = B 9 = A 10 = A 11 = A0 = B 1 = A 2 = C 3 = f 4 = B 5 = A 6 = B 7 = A 8 = B 9 = A 10 = f 11 = A 12 = C 13 = B 14 = f 15 = f 16 = C 17 = f 18 = C 19 = B 20 = C 21 = C 22 = A 23 = f 
复制代码

枚举与类有什么区别

虽然先前我们说枚举与类相似,但这二者也有很多不同之处:

  • 枚举的序列化方式与对象不同;

  • 枚举没有状态,而类的对象则有;

  • 枚举不声明构造函数,因为不需要初始化对象;

  • 枚举不能扩展其他枚举,也就意味着没有继承;

  • 不支持对象和静态属性;

  • 枚举不能用 new 操作符进行实例化;

  • 打印print_r语句的输出结果与类对象不同。

下面这段脚本中更具体地展示了这二者的差异之一,枚举不能声明类属性:

<?phpenum SortType  {  case Asc;  case Desc;  case Shuffle;    public $var = 1;} 
复制代码

脚本的报错信息为:Fatal error: Enums may not include properties

另一个差异点可以在下面这段脚本中有所体现,枚举不能被实例化:

<?phpenum SortType  {  case Asc;  case Desc;  case Shuffle;}$sortType=new SortType(); 
复制代码

脚本的报错信息为:Fatal error: Uncaught Error: Cannot instantiate enum SortType

可通过下面这段脚本的print_r中纯粹枚举与回退枚举进行更直观的对比:

<?php   enum SortType {  case Asc;  case Desc;  case Shuffle;}enum BackedSortType: int {  case Asc = 1;  case Desc = 2;  case Shuffle = 3;}print_r(SortType::Asc);print_r(BackedSortType::Desc);
复制代码

输出为:

SortType Enum ( [name] => Asc ) BackedSortType Enum:int ( [name] => Desc [value] => 2 )
复制代码

枚举也可以声明一个__toString方法。下面这段脚本在Stringable接口中实现了枚举,并给出了__toString方法的实现。

<?phpenum SortType implements Stringable {  case Asc;  case Desc;  case Shuffle;     const ASCENDING = SortType::Asc;        public function __toString(): string {        return "";  }   }   echo SortType::ASCENDING;
复制代码

脚本报错信息为:Fatal error: Enum may not include __toString

本文中,我们探讨了 PHP 8 中大部分与类相关的功能,其中包括枚举、新增类属性只读修饰符,以及构造函数参数提升。在系列文章的下一篇中,我们将探索函数与方法相关的新功能。

原文链接

PHP 8 - Classes and Enums

相关阅读:

PHP 8:注解、match 表达式及其他改进