图源:
从php 8.0.0开始,php支持枚举类型。简单地说,枚举代表一类有限的数据集,这在计算机领域非常常见,比如说用户类型(管理员,普通用户,VIP等),再比如订单类型(团队订单,个人订单等)。
在以前php不支持枚举的时候,我通常会使用类常量:
<?php
class UserType
{
const ADMINI = 0; //管理员
const NORMAL = 1; //普通用户
const VIP = 2; //vip
}
function create_user(string $name, int $type)
{
echo "create new user, name:{$name}, type:{$type}" . PHP_EOL;
}
create_user("admin", UserType::ADMINI);
create_user("test", 5);
// create new user, name:admin, type:0
// create new user, name:test, type:5
虽然这么做很方便,但是其实是有隐患的。原因是定义在类中的常量本身并不是一个真正的“封闭类型”,也就是说create_user
函数的$type
参数只能定义为int
,这就给非法传入参数提供了可能。当然我们可以添加额外的检测代码,但那样很麻烦,也不利于代码扩展。
最佳的解决方式就是引入真正的枚举:
<?php
namespace xyz\icexmoon\php_notes\ch15;
enum UserType
{
case ADMIN; //管理员
case NORMAL; //普通用户
case VIP; //vip
}
function create_user(string $name, UserType $type)
{
echo "create new user, name:{$name}, type:{$type->name}" . PHP_EOL;
}
create_user("admin", UserType::ADMIN);
// create new user, name:admin, type:ADMIN
create_user("test", 5);
// PHP Fatal error: Uncaught TypeError: xyz\icexmoon\php_notes\ch15\create_user():
在编译期就检查出了类型错误。
下面来详细介绍枚举类型。
枚举类型分为两种:纯枚举和回退枚举。
纯枚举
纯枚举就是我们上边展示过的普通形式的枚举,使用enum
关键字和case
就可以简单创建。
普通枚举是利用类来创建的,其中每一个case
定义的枚举值都是枚举类型的单例。作为单例,我们可以使用===
来比较是否为同一个枚举值:
<?php
namespace xyz\icexmoon\php_notes\ch15;
enum UserType
{
case ADMIN; //管理员
case NORMAL; //普通用户
case VIP; //vip
}
$ut_admin1 = UserType::ADMIN;
$ut_admin2 = UserType::ADMIN;
var_dump($ut_admin1 === $ut_admin2);
// bool(true)
var_dump($ut_admin1 === UserType::VIP);
// bool(false)
而===
和!==
以外的比较都是没有意义的:
var_dump($ut_admin1 > $ut_admin2);
// bool(false)
var_dump($ut_admin1 < UserType::VIP);
// bool(false)
枚举值有一个只读属性name
,可以用于打印枚举值的名称:
echo $ut_admin1->name . PHP_EOL;
// ADMIN
echo UserType::NORMAL->name . PHP_EOL;
// NORMAL
回退枚举
纯枚举是没有关联具体数值的,也就是说没法用枚举变量与具体数值比较,也没法利用某个数值来获取枚举值。虽然纯枚举很好地符合枚举类型的定义,但现实往往需要我们做出折中而非完美的设计。比如说我们数据库中表示用户类型的字段使用的是1、2、3来表示,那么枚举类值如果不能方便地进行映射,就会很麻烦。
回退枚举正是为了解决这种问题出现的。
所谓的回退,大概是指让枚举值从一个单例“回退”到一个基本数据类型。
回退枚举支持关联两种基础数据类型:int
和string
。
这里以关联int
的回退枚举举例:
<?php
namespace xyz\icexmoon\php_notes\ch15;
enum UserType: int
{
case ADMIN = 1; //管理员
case NORMAL = 2; //普通用户
case VIP = 3; //vip
}
function create_user(string $name, UserType $type)
{
echo "create new user, name:{$name}, type:{$type->name}" . PHP_EOL;
}
create_user("admin", UserType::ADMIN);
回退枚举需要在枚举声明enum enum_name:
后说明“回退类型”,并且在枚举定义中为每个枚举值指定不重复的值。
回退类型不能混用,即只能使用int
或只能使用string
,在一个枚举类型中同时使用string
和int
。
回退枚举的枚举值有一个只读属性value
,可以用于访问枚举值关联的具体数值:
echo UserType::VIP->value.PHP_EOL;
// 3
回退枚举隐式地实现了内建接口BackedEnum
,该接口包含两个静态方法:
-
from(int|string $value):self
-
tryFrom(int|string $value):self
利用枚举类型的from
和tryFrom
方法,可以使用一个具体值获取对应的枚举值。区别在于如果输入是一个非法值(对应不到枚举值),前者会抛出异常,后者则会返回一个null
。
使用from
获取对应的枚举值:
function get_user_type(int $typeFlag): ?UserType
{
try{
return UserType::from($typeFlag);
}
catch (Error $e){
echo $e->getMessage().PHP_EOL;
return null;
}
}
$ut = get_user_type(1);
$ut2 = get_user_type(5);
// 5 is not a valid backing value for enum "xyz\icexmoon\php_notes\ch15\UserType"
echo "name:{$ut->name},value:{$ut->value}".PHP_EOL;
// name:ADMIN,value:1
使用tryFrom
获取枚举值:
function get_user_type(int $typeFlag): ?UserType
{
return UserType::tryFrom($typeFlag);
}
$ut = get_user_type(1);
$ut2 = get_user_type(5);
echo "name:{$ut->name},value:{$ut->value}" . PHP_EOL;
// name:ADMIN,value:1
echo "name:{$ut2?->name},value:{$ut2?->value}" . PHP_EOL;
// name:,value:
枚举方法
可以给枚举类型添加方法,甚至是实现接口:
<?php
namespace xyz\icexmoon\php_notes\ch15\color;
interface ChineseAble
{
public function get_chinese_name(): string;
}
enum Color implements ChineseAble
{
case LIGHT_PINK; //浅粉色
case PINK; //粉色
case CRIMSON; //深红色
case DEEP_PINK; //深粉色
case BLUE; //蓝色
case LIGHT_STEE_BLUE; //亮钢蓝
public function get_chinese_name(): string
{
return match ($this) {
Color::LIGHT_PINK => '浅粉色',
Color::PINK => '粉色',
Color::CRIMSON => '深红色',
Color::DEEP_PINK => '深粉色',
Color::BLUE => '蓝色',
Color::LIGHT_STEE_BLUE => '亮蓝色',
};
}
}
function print_color(Color $color): void
{
echo "color:" . $color->name . ", Chinese name:" . $color->get_chinese_name() . PHP_EOL;
}
print_color(Color::LIGHT_PINK);
print_color(Color::PINK);
// color:LIGHT_PINK, Chinese name:浅粉色
// color:PINK, Chinese name:粉色
前面说过,枚举值是枚举类型的单例,所以枚举值可以直接使用定义的方法。此外可以给枚举类型添加public
、private
、protected
的方法,但后两者是没有意义的,因为枚举类型不能被继承。
完整的色卡表可以阅读。
静态方法
除了普通方法,还可以给枚举类型添加静态方法:
<?php
...
enum Color implements ChineseAble
{
...
public static function get_color_by_code(string $code): ?self
{
return match ($code) {
"#FFB6C1" => self::LIGHT_PINK,
"#FFC0CB" => self::PINK,
"#DC143C" => self::CRIMSON,
"#FF1493" => self::DEEP_PINK,
"#0000FF" => self::BLUE,
"#B0C4DE" => self::LIGHT_STEE_BLUE,
default => null,
};
}
}
function print_color(?Color $color): void
{
if (is_null($color)) {
echo "It's not a valid color." . PHP_EOL;
return;
}
echo "color:" . $color->name . ", Chinese name:" . $color->get_chinese_name() . PHP_EOL;
}
print_color(Color::get_color_by_code("#B0C4DE"));
print_color(Color::get_color_by_code("#000080"));
// color:LIGHT_STEE_BLUE, Chinese name:亮蓝色
// It's not a valid color.
枚举常量
枚举类型的值都应当是互斥的,也就是不能是包含关系。所以有些时候我们要表示一些其他枚举类型相关的信息的时候可能无法以添加一个新的枚举值方式,这时候可以考虑以常量的形式添加:
...
enum Color implements ChineseAble
{
const DEEP_COLOR = 1;
const LIGHT_COLOR = 2;
...
public function get_color_type(): int
{
return match ($this) {
self::LIGHT_PINK, self::PINK, self::LIGHT_STEE_BLUE => self::LIGHT_COLOR,
self::CRIMSON, self::BLUE, self::DEEP_PINK => self::DEEP_COLOR,
};
}
...
}
function print_color(?Color $color): void
{
if (is_null($color)) {
echo "It's not a valid color." . PHP_EOL;
return;
}
$colorType = match ($color->get_color_type()) {
Color::DEEP_COLOR => '深色系',
Color::LIGHT_COLOR => '浅色系',
default => '未定义色系',
};
echo "color:" . $color->name . ", Chinese name:" . $color->get_chinese_name() . ", color type:{$colorType}" . PHP_EOL;
}
print_color(Color::get_color_by_code("#B0C4DE"));
print_color(Color::get_color_by_code("#000080"));
// color:LIGHT_STEE_BLUE, Chinese name:亮蓝色, color type:浅色系
// It's not a valid color.
这个例子并不是很恰当,因为在实际使用中,ColorType
大概率会单独作为一个枚举类型来使用,而不是作为Color
枚举类型的常量。但这里依然说明了如何定义和使用枚举类型常量。
Trait
枚举是可以使用trait
的:
...
trait Test
{
public function test()
{
echo "Test::test() is called." . PHP_EOL;
}
public static function static_test()
{
echo "Test::static_test() is called." . PHP_EOL;
}
}
...
enum Color implements ChineseAble
{
use Test;
...
}
...
Color::static_test();
Color::PINK->test();
// Test::static_test() is called.
// Test::test() is called.
但相比普通的类,枚举在使用trait
时有一些限制,比如说不能使用定义了属性或静态属性的trait
,否则会报错。这是因为枚举本身是不能定义属性或静态属性的。
和对象的区别
现在来总结枚举类型和普通类的区别:
-
枚举不支持继承。
-
枚举不支持构造和析构函数。
-
枚举不支持属性或静态属性。
-
枚举不支持对象复制(因为枚举值是单例)。
-
枚举不支持魔术方法(唯一的例外是
__call
、__callStatic
、__invoke
)。 -
不能使用
new
关键字或反射机制来手动创建枚举对象。
UnitEnum接口
所有的枚举类型都隐式实现了UnitEnum
接口,该接口有一个case
静态方法。在枚举中,该方法会返回包含了所有枚举对象的数组:
...
foreach (Color::cases() as $color){
print_color($color);
}
// color:LIGHT_PINK, Chinese name:浅粉色, color type:浅色系
// color:PINK, Chinese name:粉色, color type:浅色系
// color:CRIMSON, Chinese name:深红色, color type:深色系
// color:DEEP_PINK, Chinese name:深粉色, color type:深色系
// color:BLUE, Chinese name:蓝色, color type:深色系
// color:LIGHT_STEE_BLUE, Chinese name:亮蓝色, color type:浅色系
序列化
枚举类型可以被内置函数正常序列化和反序列化:
$color = Color::BLUE;
$color = unserialize(serialize($color));
print_color($color);
// color:BLUE, Chinese name:蓝色, color type:深色系
不过序列化后的标签与类有所区别:
echo serialize(Color::BLUE) . PHP_EOL;
echo serialize(new stdClass) . PHP_EOL;
// E:45:"xyz\icexmoon\php_notes\ch15\color6\Color:BLUE";
// O:8:"stdClass":0:{}
枚举对象以E
开头,普通对象以O
开头。
此外,不能将纯枚举类型的对象转化为json
:
echo json_encode(Color::BLUE) . PHP_EOL;
上边的示例不会有任何输出。但可以将回退枚举转化为json
。
谢谢阅读。
往期内容
文章评论