1
0
mirror of https://github.com/php/doc-zh.git synced 2026-03-23 22:52:08 +01:00
Files
archived-doc-zh/language/enumerations.xml
2025-11-02 22:06:14 +08:00

975 lines
25 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="utf-8"?>
<!-- EN-Revision: 2e5f2910f3a2a95dcbdaf580ba57a0b60b072c2a Maintainer: daijie Status: ready -->
<!-- CREDITS: Luffy, mowangjuanzi -->
<chapter xml:id="language.enumerations" xmlns="http://docbook.org/ns/docbook">
<title>枚举</title>
<sect1 xml:id="language.enumerations.overview">
<title>枚举概览</title>
<?phpdoc print-version-for="enumerations"?>
<para>
枚举,或称 “Enum”能够让开发者自定义类型为一系列可能的离散值中的一个。
在定义领域模型中很有用它能够“隔离无效状态”making invalid states unrepresentable
</para>
<para>
枚举以各种不同功能的形式出现在诸多语言中。
在 PHP 中, 枚举是一种特殊类型的对象。Enum 本身是一个类Class
它的各种条目case是这个类的单例对象意味着也是个有效对象
—— 包括类型的检测,能用对象的地方,也可以用它。
</para>
<para>
最常见的枚举例子是内置的 boolean 类型,
该枚举类型有两个有效值 &true;&false;
Enum 使开发者能够任意定义出用户自己的、足够健壮的枚举。
</para>
</sect1>
<sect1 xml:id="language.enumerations.basics">
<title>枚举基础</title>
<para>
Enum 类似 class它和 class、interface、trait 共享同样的命名空间。
也能用同样的方式自动加载。
一个 Enum 定义了一种新的类型,它有固定、数量有限、可能的合法值。
</para>
<programlisting role="php">
<![CDATA[
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
?>
]]>
</programlisting>
<para>
以上声明了新的枚举类型 <literal>Suit</literal>,仅有四个有效的值:
<literal>Suit::Hearts</literal><literal>Suit::Diamonds</literal>
<literal>Suit::Clubs</literal><literal>Suit::Spades</literal>
变量可以赋值为以上有效值里的其中一个。
函数可以检测枚举类型,这种情况下只能传入类型的值。
</para>
<programlisting role="php">
<![CDATA[
<?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');
?>
]]>
</programlisting>
<para>
一个枚举可以定义零个或多个<literal>case</literal>,且没有最大数量限制。
虽然零个 case 的 enum 没什么用处,但在语法上也是有效的。
</para>
<para>
枚举条目的语法规则适用于 PHP 中的任何标签,参见<link linkend="language.constants">常量</link>
</para>
<para>
默认情况下枚举的条目case本质上不是标量。
就是说 <literal>Suit::Hearts</literal> 不等同于 <literal>"0"</literal>
其实,本质上每个条目是该名称对象的单例。具体来说:
</para>
<programlisting role="php">
<![CDATA[
<?php
$a = Suit::Spades;
$b = Suit::Spades;
$a === $b; // true
$a instanceof Suit; // true
?>
]]>
</programlisting>
<para>
由于对象间的大小比较毫无意义,这也意味着 enum 值从来不会 <literal>&lt;</literal><literal>&gt;</literal>
其他值。当 enum 的值用于比较时,总是返回 &false;
</para>
<para>
这类没有关联数据的条目case被称为“纯粹条目”Pure Case
仅包含纯粹 Case 的 Enum 被称为纯粹枚举Pure Enum
</para>
<para>
枚举类型里所有的纯粹条目都是自身的实例。枚举类型在内部的实现形式是 class。
</para>
<para>
所有的 case 有个只读的属性 <literal>name</literal>
它大小写敏感,是 case 自身的名称。
</para>
<programlisting role="php">
<![CDATA[
<?php
print Suit::Spades->name;
// 输出 "Spades"
?>
]]>
</programlisting>
<para>
如果名称是动态获取的,也可以使用 <function>defined</function><function>constant</function>
函数来检查枚举成员是否存在或读取其值。然而,这种方法并不推荐,因为对于大多数使用场景,使用<link
linkend="language.enumerations.backed">带值枚举</link>即可满足需求。
</para>
</sect1>
<sect1 xml:id="language.enumerations.backed">
<title>带值(回退)枚举</title>
<para>
默认情况下,枚举成员没有对应的标量值。它们仅仅是单例对象。然而,在许多情况下,枚举成员需要能够与数据库或类似的数据存储进行往返操作,因此内置一个本质上定义的标量值(从而可以轻松序列化)是非常有用的。
</para>
<para>定义标量形式的枚举的语法如下:</para>
<programlisting role="php">
<![CDATA[
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
?>
]]>
</programlisting>
<para>
由于有标量的条目回退Backed到一个更简单值又叫回退条目Backed Case
包含所有回退条目的 Enum 又叫“回退 Enum”Backed Enum
回退 Enum 只能包含回退条目。
纯粹 Enum 只能包含纯粹条目。
</para>
<para>
回退枚举仅能回退到 <literal>int</literal><literal>string</literal> 里的一种类型,
且同时仅支持使用一种类型(就是说,不能联合 <literal>int|string</literal>)。
如果枚举为标量形式,所有的条目必须明确定义唯一的标量值。
无法自动生成标量(比如:连续的数字)。
回退条目必须是唯一的;两个回退条目不能有相同的标量。
然而,也可以用常量引用到条目,实际上是创建了个别名。
参见 <link linkend="language.enumerations.constants">枚举常量</link>
</para>
<para>
等值可以是常量标量表达式。PHP 8.2.0
之前,必须是文字或文字表达式。这意味着不支持常量和常量表达式。也就是说,允许
<code>1 + 1</code>,但不允许 <code>1 + SOME_CONST</code>
</para>
<para>
回退条目有个额外的只读属性 <literal>value</literal>
它是定义时指定的值。
</para>
<programlisting role="php">
<![CDATA[
<?php
print Suit::Clubs->value;
// 输出 "C"
?>
]]>
</programlisting>
<para>
为了确保 <literal>value</literal> 的只读性,
无法将变量传引用给它。
也就是说,以下会抛出错误:
</para>
<programlisting role="php">
<![CDATA[
<?php
$suit = Suit::Clubs;
$ref = &$suit->value;
// Error: Cannot acquire reference to property Suit::$value
?>
]]>
</programlisting>
<para>
回退枚举实现了内置的 <interfacename>BackedEnum</interfacename> interface
暴露了两个额外的方法:
</para>
<simplelist>
<member>
<literal>from(int|string): self</literal> 能够根据标量返回对应的枚举条目。
如果找不到该值,会抛出 <classname>ValueError</classname>
主要用于输入标量为可信的情况,使用一个不存在的枚举值,可以考虑为需终止应用的错误。
</member>
<member>
<literal>tryFrom(int|string): ?self</literal> 能够根据标量返回对应的枚举条目。
如果找不到该值,会返回 <literal>null</literal>
主要用于输入标量不可信的情况,调用者需要自己实现默认值的逻辑或错误的处理。
</member>
</simplelist>
<para>
<literal>from()</literal><literal>tryFrom()</literal> 方法也遵循基本的严格/松散类型规则。
系统在弱类型模式下接受传入 integer 和 string并自动强制转换对应值。
传入 float 也能运行,并且强制转换。
在严格类型模式下,为 string 回退枚举的 <literal>from()</literal> 传入 integer 会导致
<classname>TypeError</classname>反之亦然float 都会出现有问题。
其他所有的参数类型,在以上所有模式中都会抛出 TypeError。
</para>
<programlisting role="php">
<![CDATA[
<?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;
?>
]]>
</programlisting>
<para>手动为回退枚举定义 <literal>from()</literal><literal>tryFrom()</literal> 方法会导致 fatal 错误。</para>
</sect1>
<sect1 xml:id="language.enumerations.methods">
<title>枚举方法</title>
<para>
枚举(包括纯粹枚举、回退枚举)还能包含方法,
也能实现 interface。
如果 Enum 实现了 interface则其中的条目也能接受 interface 的类型检测。
</para>
<programlisting role="php">
<![CDATA[
<?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"
?>
]]>
</programlisting>
<para>
在这例子中,<literal>Suit</literal> 所有的四个实例具有两个方法:
<literal>color()</literal><literal>shape()</literal>
目前的调用代码和类型检查,和其他对象实例的行为完全一致。
</para>
<para>
在回退枚举中interface 的声明紧跟回退类型的声明之后。
</para>
<programlisting role="php">
<![CDATA[
<?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',
};
}
}
?>
]]>
</programlisting>
<para>
在方法中,定义了 <literal>$this</literal> 变量,它引用到了条目实例。
</para>
<para>
方法中可以任意复杂,但一般的实践中,往往会返回静态的值,
或者为 <literal>$this</literal> &match; 各种情况并返回不同的值。
</para>
<para>
注意,在本示例中,更好的数据模型实践是再定一个包含 Red 和 Black
枚举值的 <literal>SuitColor</literal> 枚举,并且返回它作为代替。
然而这会让本示例复杂化。
</para>
<para>
以上的层次在逻辑中类似于下面的 class 结构(虽然这不是它实际运行的代码):
</para>
<programlisting role="php">
<![CDATA[
<?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() 方法
// 参考 “枚举值清单” 章节
}
}
?>
]]>
</programlisting>
<para>
尽管 enum 可以包括 public、private、protected 的方法,
但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。
</para>
</sect1>
<sect1 xml:id="language.enumerations.static-methods">
<title>枚举静态方法</title>
<para>
枚举也能有静态方法。
在枚举中静态方法主要用于取代构造器,如:
</para>
<programlisting role="php">
<![CDATA[
<?php
enum Size
{
case Small;
case Medium;
case Large;
public static function fromLength(int $cm): self
{
return match(true) {
$cm < 50 => self::Small,
$cm < 100 => self::Medium,
default => self::Large,
};
}
}
?>
]]>
</programlisting>
<para>
虽然 enum 可以包括 public、private、protected 的静态方法,
但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。
</para>
</sect1>
<sect1 xml:id="language.enumerations.constants">
<title>枚举常量</title>
<para>
虽然 enum 可以包括 public、private、protected 的常量,
但由于它不支持继承,因此在实践中 private 和 protected 效果是相同的。
</para>
<para>枚举常量可以引用枚举条目:</para>
<programlisting role="php">
<![CDATA[
<?php
enum Size
{
case Small;
case Medium;
case Large;
public const Huge = self::Large;
}
?>
]]>
</programlisting>
</sect1>
<sect1 xml:id="language.enumerations.traits">
<title>Trait</title>
<para>枚举也能使用 trait行为和 class 一样。
留意在枚举中 <literal>use</literal> trait 不允许包含属性。
只能包含方法、静态方法和常量。包含属性的 trait 会导致 fatal 错误。
</para>
<programlisting role="php">
<![CDATA[
<?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',
};
}
}
?>
]]>
</programlisting>
</sect1>
<sect1 xml:id="language.enumerations.expressions">
<title>常量表达式的枚举值</title>
<para>
由于用 enum 自身的常量表示条目,它们可当作静态值,用于绝大多数常量表达式:
属性默认值、变量默认值、参数默认值、全局和类常量。
他们不能用于其他 enum 枚举值,但通常的常量可以引用枚举条目。
</para>
<para>
然而,因为不能保证结果值绝对不变,也不能避免调用方法时带来副作用,
所以枚举里类似 <classname>ArrayAccess</classname> 这样的隐式魔术方法调用无法用于静态定义和常量定义。
常量表达式还是不能使用函数调用、方法调用、属性访问。
</para>
<programlisting role="php">
<![CDATA[
<?php
// 这是完全合法的 Enum 定义
enum Direction implements ArrayAccess
{
case Up;
case Down;
public function offsetExists($offset): bool
{
return false;
}
public function offsetGet($offset): mixed
{
return null;
}
public function offsetSet($offset, $value): void
{
throw new Exception();
}
public function offsetUnset($offset): void
{
throw new Exception();
}
}
class Foo
{
// 可以这样写。
const DOWN = Direction::Down;
// 由于它是不确定的,所以不能这么写。
const UP = Direction::Up['short'];
// Fatal error: Cannot use [] on enums in constant expression
}
// 由于它不是一个常量表达式,所以是完全合法的
$x = Direction::Up['short'];
var_dump("\$x is " . var_export($x, true));
$foo = new Foo();
?>
]]>
</programlisting>
</sect1>
<sect1 xml:id="language.enumerations.object-differences">
<title>和对象的差异</title>
<para>
尽管 enum 基于类和对象,但它们不完全支持对象相关的所有功能。
尤其是枚举条目不能有状态。
</para>
<simplelist>
<member>禁止构造、析构函数。</member>
<member>不支持继承。无法 extend 一个 enum。</member>
<member>不支持静态属性和对象属性。</member>
<member>由于枚举条目是单例对象,所以不支持对象复制。</member>
<member>除了下面列举项,不能使用<link linkend="language.oop5.magic">魔术方法</link></member>
<member>枚举必须在使用前被声明。</member>
</simplelist>
<para>以下对象功能可用,功能和其他对象一致:</para>
<simplelist>
<member>Public、private、protected 方法。</member>
<member>Public、private、protected 静态方法。</member>
<member>Public、private、protected 类常量。</member>
<member>enum 可以 implement 任意数量的 interface。</member>
<member>
枚举和它的条目都可以附加 <link linkend="language.attributes">注解</link>
目标过滤器 <constant>TARGET_CLASS</constant> 包括枚举自身。
目标过滤器 <constant>TARGET_CLASS_CONST</constant> 包括枚举条目。
</member>
<member>
魔术方法:<link linkend="object.call">__call</link><link linkend="object.callstatic">__callStatic</link>
<link linkend="object.invoke">__invoke</link>
</member>
<member>常量 <constant>__CLASS__</constant><constant>__FUNCTION__</constant> 的功能和平时无差别</member>
</simplelist>
<para>
枚举类型的魔术常量 <literal>::class</literal> 和对象完全一样,
它是个包含命名空间的类型名称。
由于枚举条目是枚举类型的一个实例,因此它的 <literal>::class</literal>
也和枚举类型一样。
</para>
<para>
此外,不能用 <literal>new</literal> 直接实例化枚举条目,
也不能用 <methodname>ReflectionClass::newInstanceWithoutConstructor</methodname> 反射实例化。
这么做都会导致错误。
</para>
<programlisting role="php">
<![CDATA[
<?php
$clovers = new Suit();
// Error: Cannot instantiate enum Suit
$horseshoes = (new ReflectionClass(Suit::class))->newInstanceWithoutConstructor()
// Error: Cannot instantiate enum Suit
?>
]]>
</programlisting>
</sect1>
<sect1 xml:id="language.enumerations.listing">
<title>枚举值清单</title>
<para>
无论是纯粹枚举还是回退枚举,都实现了一个叫 <interfacename>UnitEnum</interfacename> 的内部接口。
<literal>UnitEnum</literal> 包含了一个静态方法:
<literal>cases()</literal>
按照声明中的顺序,<literal>cases()</literal> 返回了打包的 array包含全部定义的条目。
</para>
<programlisting role="php">
<![CDATA[
<?php
Suit::cases();
// 产生: [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades]
?>
]]>
</programlisting>
<para>为 Enum 手动定义 <literal>cases()</literal> 方法会导致 fatal 错误。</para>
</sect1>
<sect1 xml:id="language.enumerations.serialization">
<title>序列化</title>
<para>
枚举的序列化不同于对象。
尤其是它们有新的序列化代码:
<literal>"E"</literal>,指示了 enum 条目名称。
然后反序列化动作能够设置变量为现有的单例值。
确保那样:
</para>
<programlisting role="php">
<![CDATA[
<?php
Suit::Hearts === unserialize(serialize(Suit::Hearts));
print serialize(Suit::Hearts);
// E:11:"Suit:Hearts";
?>
]]>
</programlisting>
<para>
如果枚举和它的条目在反序列化时,无法匹配序列化的值,
会导致 warning 警告,并返回 &false;
</para>
<para>
把纯粹枚举序列化为 JSON 将会导致错误。
把回退枚举序列化为 JSON 时,仅会用标量值的形式,以合适的类型表达。
可通过实现 <classname>JsonSerializable</classname> 来重载序列化行为。
</para>
<para>对于 <function>print_r</function>,输出的枚举条目略微不同于对象,
能减少迷惑。
</para>
<programlisting role="php">
<![CDATA[
<?php
enum Foo {
case Bar;
}
enum Baz: int {
case Beep = 5;
}
print_r(Foo::Bar);
print_r(Baz::Beep);
/* 产生
Foo Enum (
[name] => Bar
)
Baz Enum:int {
[name] => Beep
[value] => 5
}
*/
?>
]]>
</programlisting>
</sect1>
<sect1 xml:id="language.enumerations.object-differences.inheritance">
<title>为什么枚举不可扩展</title>
<simpara>
类在方法有契约:
</simpara>
<programlisting role="php">
<![CDATA[
<?php
class A {}
class B extends A {}
function foo(A $a) {}
function bar(B $b) {
foo($b);
}
?>
]]>
</programlisting>
<simpara>
这段代码类型安全,因为 B 遵循 A 的契约,并通过协变/逆变的逻辑,将会保留任何对方法的期望,除了异常。
</simpara>
<simpara>
枚举在其选项上有契约,而不是方法:
</simpara>
<programlisting role="php">
<![CDATA[
<?php
enum ErrorCode {
case SOMETHING_BROKE;
}
function quux(ErrorCode $errorCode)
{
// 编写时,此代码似乎涵盖了所有情况
match ($errorCode) {
ErrorCode::SOMETHING_BROKE => true,
};
}
?>
]]>
</programlisting>
<simpara>
在函数 <code>quux</code> 中,&match; 语句可以进行静态分析,以涵盖 ErrorCode 中的所有情况。
</simpara>
<simpara>
但是想一下,如果允许扩展枚举:
</simpara>
<programlisting role="php">
<![CDATA[
<?php
// 当枚举不是 final 时考虑做的实验代码。
// 注意在 PHP 中实际不起作用。
enum MoreErrorCode extends ErrorCode {
case PEBKAC;
}
function fot(MoreErrorCode $errorCode) {
quux($errorCode);
}
fot(MoreErrorCode::PEBKAC);
?>
]]>
</programlisting>
<simpara>
根据正常的继承规则,继承另一个类的类将通过类型检查。
</simpara>
<simpara>
问题在于 <code>quux()</code> 中的 &match; 语句不再涵盖所有情况。因为它不知道 <code>MoreErrorCode::PEBKAC</code>,所以匹配语句会抛出异常。
</simpara>
<simpara>
因此,枚举是 final不能扩展。
</simpara>
</sect1>
<sect1 xml:id="language.enumerations.examples">
&reftitle.examples;
<para>
<example>
<title>值受限的基本用法</title>
<programlisting role="php">
<![CDATA[
<?php
enum SortOrder
{
case Asc;
case Desc;
}
function query($fields, $filter, SortOrder $order = SortOrder::Asc)
{
/* ... */
}
?>
]]>
</programlisting>
<para>
由于确保 <literal>$order</literal> 不是 <literal>SortOrder::Asc</literal>
就是 <literal>SortOrder::Desc</literal>,所以 <literal>query()</literal> 函数能安全处理。
因为其他任意值都会导致 <classname>TypeError</classname>
所以不需要额外的错误检查。
</para>
</example>
</para>
<para>
<example>
<title>值排他的高级用法</title>
<programlisting role="php">
<![CDATA[
<?php
enum UserStatus: string
{
case Pending = 'P';
case Active = 'A';
case Suspended = 'S';
case CanceledByUser = 'C';
public function label(): string
{
return match($this) {
self::Pending => 'Pending',
self::Active => 'Active',
self::Suspended => 'Suspended',
self::CanceledByUser => 'Canceled by user',
};
}
}
?>
]]>
</programlisting>
<para>
这个例子中,用户的状态是 <literal>UserStatus::Pending</literal>
<literal>UserStatus::Active</literal><literal>UserStatus::Suspended</literal>
<literal>UserStatus::CanceledByUser</literal> 中的一个,具有独占性。
函数可以根据 <literal>UserStatus</literal> 设置参数类型,仅支持这四种值。
</para>
<para>
所有四个值都有一个 <literal>label()</literal> 方法,返回了人类可读的字符串。
它独立于等同于标量的“机器名”。
机器名用于类似数据库字段或 HTML 选择框这样的地方。
</para>
<programlisting role="php">
<![CDATA[
<?php
foreach (UserStatus::cases() as $case) {
printf('<option value="%s">%s</option>\n', $case->value, $case->label());
}
?>
]]>
</programlisting>
</example>
</para>
</sect1>
</chapter>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:1
sgml-indent-data:t
indent-tabs-mode:nil
sgml-parent-document:nil
sgml-default-dtd-file:"~/.phpdoc/manual.ced"
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
vim600: syn=xml fen fdm=syntax fdl=2 si
vim: et tw=78 syn=sgml
vi: ts=1 sw=1
-->