-
Notifications
You must be signed in to change notification settings - Fork 109
Expand file tree
/
Copy pathenumerations.xml
More file actions
974 lines (789 loc) · 24.6 KB
/
enumerations.xml
File metadata and controls
974 lines (789 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
<?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><</literal> 或 <literal>></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
-->