On this page

PHP 枚举

枚举

Enum 类似 class,它和 class、interface、trait 共享同样的命名空间。 也能用同样的方式自动加载。 一个 Enum 定义了一种新的类型,它有固定、数量有限、可能的合法值。


<?php
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}
?>

以上声明了新的枚举类型 Suit,仅有四个有效的值: Suit::HeartsSuit::DiamondsSuit::ClubsSuit::Spades。 变量可以赋值为以上有效值里的其中一个。 函数可以检测枚举类型,这种情况下只能传入类型的值。


<?php
function pick_a_card(Suit $suit) { ... }

$val = Suit::Diamonds;

// OK
pick_a_card($val);
// OK
pick_a_card(Suit::Clubs);
// TypeError: pick_a_card(): Argument #1 ($suit) must be of type Suit, string given
pick_a_card('Spades');
?>

一个枚举可以定义零个或多个case,且没有最大数量限制。 虽然零个 case 的 enum 没什么用处,但在语法上也是有效的。

枚举条目的语法规则适用于 PHP 中的任何标签,参见[常量](https://www.php.net/manual/zh/language.constants.php)。

默认情况下,枚举的条目(case)本质上不是标量。 就是说 Suit::Hearts 不等同于 "0"。 其实,本质上每个条目是该名称对象的单例。具体来说:


<?php
$a = Suit::Spades;
$b = Suit::Spades;

$a === $b; // true

$a instanceof Suit;  // true
?>

由于对象间的大小比较毫无意义,这也意味着 enum 值从来不会 <> 其他值。 当 enum 的值用于比较时,总是返回 false

这类没有关联数据的条目(case),被称为“纯粹条目”(Pure Case)。 仅包含纯粹 Case 的 Enum 被称为纯粹枚举(Pure Enum)。

枚举类型里所有的纯粹条目都是自身的实例。 枚举类型在内部的实现形式是一个 class。

所有的 case 有个只读的属性 name。 它大小写敏感,是 case 自身的名称。


<?php
print Suit::Spades->name;
// 输出 "Spades"
?>

回退(Backed)枚举

默认情况下枚举条目实现形式不是标量。 它们是纯粹的对象实例。 不过,很多时候也需要在数据库、数据存储对象中来回读写枚举条目。 因此,能够内置支持标量形式也很有用(更易序列化)。

按以下语法,定义标量形式的枚举:


<?php
enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}
?>

由于有标量的条目回退(Backed)到一个更简单值,又叫回退条目(Backed Case)。 包含所有回退条目的 Enum 又叫“回退 Enum”(Backed Enum)。 回退 Enum 只能包含回退条目。 纯粹 Enum 只能包含纯粹条目。

回退枚举仅能回退到 intstring 里的一种类型, 且同时仅支持使用一种类型(就是说,不能联合 int|string)。 如果枚举为标量形式,所有的条目必须明确定义唯一的标量值。 无法自动生成标量(比如:连续的数字)。 回退条目必须是唯一的;两个回退条目不能有相同的标量。 然而,也可以用常量引用到条目,实际上是创建了个别名。 参见 枚举常量

条目等同的值,必须是个字面量或它的表达式。 不能是常量和常量表达式。 换言之,允许 1 + 1, 不允许 1 + SOME_CONST

回退条目有个额外的只读属性 value, 它是定义时指定的值。


<?php
print Suit::Clubs->value;
// 输出 "C"
?>

为了确保 value 的只读性, 无法将变量传引用给它。 也就是说,以下会抛出错误:


<?php
$suit = Suit::Clubs;
$ref = &$suit->value;
// Error: Cannot acquire reference to property Suit::$value
?>

回退枚举实现了内置的 BackedEnum interface, 暴露了两个额外的方法:

  • from(int|string): self 能够根据标量返回对应的枚举条目。
    如果找不到该值,会抛出 [ValueError](https://www.php.net/manual/zh/class.valueerror.php)。
    主要用于输入标量为可信的情况,使用一个不存在的枚举值,可以考虑为需终止应用的错误。
    
  • tryFrom(int|string): ?self 能够根据标量返回对应的枚举条目。
    如果找不到该值,会返回 `null`。
    主要用于输入标量不可信的情况,调用者需要自己实现默认值的逻辑或错误的处理。
    

from()tryFrom() 方法也遵循基本的严格/松散类型规则。 系统在弱类型模式下接受传入 integer 和 string,并自动强制转换对应值。 传入 float 也能运行,并且强制转换。 在严格类型模式下,为 string 回退枚举的 from() 传入 integer 会导致 TypeError,反之亦然;float 都会出现有问题。 其他所有的参数类型,在以上所有模式中都会抛出 TypeError。


<?php
$record = get_stuff_from_database($id);
print $record['suit'];

$suit =  Suit::from($record['suit']);
// 无效数据抛出 ValueError:"X" is not a valid scalar value for enum "Suit"
print $suit->value;

$suit = Suit::tryFrom('A') ?? Suit::Spades;
// 无效数据返回 null,因此会用 Suit::Spades 代替。
print $suit->value;
?>

手动为回退枚举定义 from()tryFrom() 方法会导致 fatal 错误。


枚举方法

枚举(包括纯粹枚举、回退枚举)还能包含方法, 也能实现 interface。 如果 Enum 实现了 interface,则其中的条目也能接受 interface 的类型检测。


<?php
interface Colorful
{
    public function color(): string;
}

enum Suit implements Colorful
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;

    // 满足 interface 契约。
    public function color(): string
    {
        return match($this) {
            Suit::Hearts, Suit::Diamonds => 'Red',
            Suit::Clubs, Suit::Spades => 'Black',
        };
    }

    // 不是 interface 的一部分;也没问题
    public function shape(): string
    {
        return "Rectangle";
    }
}

function paint(Colorful $c) { ... }

paint(Suit::Clubs);  // 正常

print Suit::Diamonds->shape(); // 输出 "Rectangle"
?>

在这例子中,Suit 所有的四个实例具有两个方法: color()shape()。 目前的调用代码和类型检查,和其他对象实例的行为完全一致。

在回退枚举中,interface 的声明紧跟回退类型的声明之后。


<?php
interface Colorful
{
    public function color(): string;
}

enum Suit: string implements Colorful
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';

    // 满足 interface 的契约。
    public function color(): string
    {
        return match($this) {
            Suit::Hearts, Suit::Diamonds => 'Red',
            Suit::Clubs, Suit::Spades => 'Black',
        };
    }
}
?>

在方法中,定义了 $this 变量,它引用到了条目实例。

方法中可以任意复杂,但一般的实践中,往往会返回静态的值, 或者为 $this match 各种情况并返回不同的值。

注意,在本示例中,更好的数据模型实践是再定一个包含 Red 和 Black 枚举值的 SuitColor 枚举,并且返回它作为代替。 然而这会让本示例复杂化。

以上的层次在逻辑中类似于下面的 class 结构(虽然这不是它实际运行的代码):


<?php
interface Colorful
{
    public function color(): string;
}

final class Suit implements UnitEnum, Colorful
{
    public const Hearts = new self('Hearts');
    public const Diamonds = new self('Diamonds');
    public const Clubs = new self('Clubs');
    public const Spades = new self('Spades');

    private function __construct(public readonly string $name) {}

    public function color(): string
    {
        return match($this) {
            Suit::Hearts, Suit::Diamonds => 'Red',
            Suit::Clubs, Suit::Spades => 'Black',
        };
    }

    public function shape(): string
    {
        return "Rectangle";
    }

    public static function cases(): array
    {
        // 不合法的方法,Enum 中不允许手动定义 cases() 方法
        // 参考 “枚举值清单” 章节
    }
}
?>

尽管 enum 可以包括 public、private、protected 的方法, 但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。


枚举静态方法

枚举也能有静态方法。 在枚举中静态方法主要用于取代构造器,如:


<?php
enum Size
{
    case Small;
    case Medium;
    case Large;

    public static function fromLength(int $cm): static
    {
        return match(true) {
            $cm < 50 => static::Small,
            $cm < 100 => static::Medium,
            default => static::Large,
        };
    }
}
?>

仅管 enum 可以包括 public、private、protected 的静态方法, 但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。


枚举常量

仅管 enum 可以包括 public、private、protected 的常量, 但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。

枚举常量可以引用枚举条目:


<?php
enum Size
{
    case Small;
    case Medium;
    case Large;

    public const Huge = self::Large;
}
?>

Trait

枚举也能使用 trait,行为和 class 一样。 留意在枚举中 use trait 不允许包含属性。 只能包含方法、静态方法。 包含属性的 trait 会导致 fatal 错误。


<?php
interface Colorful
{
    public function color(): string;
}

trait Rectangle
{
    public function shape(): string {
        return "Rectangle";
    }
}

enum Suit implements Colorful
{
    use Rectangle;

    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;

    public function color(): string
    {
        return match($this) {
            Suit::Hearts, Suit::Diamonds => 'Red',
            Suit::Clubs, Suit::Spades => 'Black',
        };
    }
}
?>

常量表达式的枚举值

由于用 enum 自身的常量表示条目,它们可当作静态值,用于绝大多数常量表达式:
属性默认值、变量默认值、参数默认值、全局和类常量。
他们不能用于其他 enum 枚举值,但通常的常量可以引用枚举条目。

然而,因为不能保证结果值绝对不变,也不能避免调用方法时带来副作用, 所以枚举里类似 ArrayAccess 这样的隐式魔术方法调用无法用于静态定义和常量定义。 常量表达式还是不能使用函数调用、方法调用、属性访问。


<?php
// 这是完全合法的 Enum 定义
enum Direction implements ArrayAccess
{
    case Up;
    case Down;

    public function offsetGet($val) { ... }
    public function offsetExists($val) { ... }
    public function offsetSet($val) { throw new Exception(); }
    public function offsetUnset($val) { throw new Exception(); }
}

class Foo
{
    // 可以这样写。
    const Bar = Direction::Down;

    // 由于它是不确定的,所以不能这么写。
    const Bar = Direction::Up['short'];
    // Fatal error: Cannot use [] on enums in constant expression
}

// 由于它不是一个常量表达式,所以是完全合法的
$x = Direction::Up['short'];
?>