为PHP RFC枚举

2020-12-05 16:49:46

该RFC将枚举引入PHP。该RFC的范围仅限于“单位枚举”,即本身就是一个值的枚举,而不仅仅是简单的原始常量的花哨语法,并且不包括其他关联信息。此功能为数据建模,自定义类型定义和monad样式的行为提供了大大扩展的支持。枚举使“使无效状态无法表现”的建模技术成为可能,这将导致代码更健壮,而无需进行详尽的测试。

许多语言都支持各种枚举。我们对各种语言进行的一项调查发现,它们可以分为三类:花哨常量,花哨对象和完整代数数据类型(ADT)。

该RFC是引入完整代数数据类型的一项较大工作的一部分。它实现枚举的“ Fancy Objects”变体,以便将来的RFC可以将其扩展为完整的ADT。它在概念和语义上均从Swift,Rust和Kotlin提取,尽管两者均未直接建模。

枚举最流行的情况是布尔值,它是具有合法值true和false的枚举类型。该RFC允许开发人员定义自己的任意健壮的枚举。

该RFC引入了新的语言构造enum。枚举与类相似,并且与类,接口和特征共享相同的名称空间。它们同样可以自动加载。枚举定义了一个新类型,该类型具有固定的有限数量的可能合法值。

该声明将创建一个名为Suit的新枚举类型,该类型具有四个且只有四个合法值:Suit :: Hearts,Suit :: Diamonds,Suit :: Clubs和Suit :: Spades。可以将变量分配给这些合法值之一。可以根据枚举类型对函数进行类型检查,在这种情况下,只能传递该类型的值。

$ val =西装::钻石;函数pick_a_card(Suit $ suit){...} pick_a_card($ val); // OKpick_a_card(Suit :: Clubs); // OKpick_a_card(' Spades'); //抛出TypeError

枚举可能具有零个或多个案例定义,没有最大值。零大小写枚举在语法上是有效的,即使它没有用。

案例本质上不受原始值的支持。也就是说,Suit :: Hearts不等于0。相反,每种情况都由具有该名称的单例对象支持。这意味着:

$ a =西装::黑桃; $ b =西装::黑桃; $ a === $ b; // true $ a Suitof的实例; // true $ a instanceof Suit ::黑桃; //正确

由于枚举类型和枚举案例都是使用类实现的,因此它们可能采用方法。枚举类型还可以实现一个接口,所有案例都必须直接或间接地实现该接口。枚举案例可能无法自行实现接口。

接口Colorful {公共功能color():字符串; }枚举西装实现Colorful {case Hearts {公共功能color():字符串{return" Red" ; }} case Diamonds {公共功能color():字符串{return" Red" ; }} case Clubs {公共功能颜色():字符串{return" Black" ; }} case Spades {公共功能color():字符串{return" Black" ; }}公共功能shape():字符串{return" Rectangle" ; }} function paint(Colorful $ c){...} paint(Suit :: Clubs); //作品

在此示例中,所有四个Enum案例都将具有从Suit继承的方法形状,并且都将具有自己的方法颜色,它们将自己实现。案例方法可以任意复杂,其功能与任何其他方法相同。

在Case方法的内部,$ this变量被定义并引用Case实例。

请注意,在这种情况下,最好还定义一个值为Red和Black的SuitColor枚举类型,然后返回该值,这将是更好的数据建模实践。但是,这会使该示例复杂化。

接口Colorful {公共功能color():字符串; }抽象类Suit实现了Colorful {公共函数shape():字符串{return" Rectangle" ; }}类Hearts扩展Suit {公共功能color():字符串{return" Red" ; }}类Diamonds扩展了Suit {公共功能color():字符串{return" Red" ; }}类Club扩展Suit {公共功能颜色():字符串{return" Black" ; }}类Spades扩展了Suit {公共功能color():字符串{return" Black" ; }}

枚举本身对静态方法的使用主要用于替代构造函数。例如:

枚举大小{大小小;案例中等;案例大;公共静态函数fromLength(int $ cm){返回match(true){$ cm< 50 =>静态::小,$ cm< 100 =>静态::中,默认=>静态::大,}; }}

尽管枚举是使用幕后的类实现的,并且具有许多语义,但是某些对象样式的功能仍被禁止。这些在枚举范围内没有意义,它们的价值值得商is(但将来可能会增加),或者它们的语义不清楚。

如果您需要任何一种功能,那么已经存在的类是上乘的选择。

以下对象功能是可用的,其行为与对任何其他对象的行为相同:

公共,私有和受保护的方法。 (由于不允许继承,因此Case的受保护方法实际上与private相同。Enum本身的私有方法不能被Case的方法访问。)

Enum类型上的:: class魔术常数的计算结果为类型名称,包括与对象完全相同的任何名称空间。

Case上的:: class魔术常数计算为Type的FQCN,后跟::,后跟案例的名称。例如,Foo \ Bar \ Baz \ Suit :: Spades。

默认情况下,枚举个案没有原始等效项。它们只是单例对象。但是,在很多情况下,枚举用例需要能够往返于数据库或类似的数据存储,因此在内部定义一个内置的原语(并因此可微序列化)等效项很有用。

枚举Suit:字符串{case Hearts =' H' ;案例钻石=' D' ;案例俱乐部=' C' ;案例黑桃=' S' ; }

支持int,string或float的原始后备类型,并且给定的枚举一次仅支持一种类型。 (即,没有int | string的并集。)如果将枚举标记为具有基本原语,则所有情况都必须具有明确定义的唯一基本原语。没有自动生成的基本等价物(例如,顺序整数)。

当在原始环境中使用时,原始等效案例会自动向下转换到其原始。例如,当用于打印时。

打印西服::俱乐部; //打印" C"打印"希望我能绘制" 。西装::黑桃; //打印"我希望我画一张S"。

将原始案例传递给原始类型的参数或返回值将在弱类型输入模式下生成原始值,并在严格类型输入模式下生成TypeError。

原始支持的枚举还具有自动生成的静态方法from()。 from()方法将从原语向上广播到其对应的枚举案例。没有匹配的Case的无效基元将引发ValueError。

不允许以原始数据为后的Case定义__toString()方法,因为这会与原始值本身产生混淆。但是,原始支持的Cases可以像其他枚举一样具有其他方法:

枚举Suit:字符串{case Hearts =' H' ;案例钻石=' D' ;案例俱乐部=' C' ;案例黑桃=' S' {public function color():字符串{return' Black' ; }}公共功能color():字符串{// ...}}

枚举本身具有自动生成的静态方法case()。 cases()以词汇顺序返回所有已定义Cases的数组。

如果枚举没有等效的原语,则将打包该数组(从0开始顺序索引)。如果枚举具有与原语等效的键,则键将是每个枚举的对应原语。如果枚举类型为float,则键将呈现为字符串。 (因此,等于1.5的原始等效项将导致键为“ 1.5”。)

枚举和格可以像其他语言构造一样附加属性。 Attribute类定义了两个附加的目标常量:TARGET_ENUM仅针对Enum本身,而TARGET_CASE具体针对Enum Case。

匹配表达式根据枚举值提供了一种自然而方便的分支逻辑方法。由于单元案例的每个实例都是单例,因此它将始终通过身份检查。因此:

$ val =西装::钻石; $ str = match($ val){西装::黑桃=> "士兵的剑" ,西服::俱乐部=> "战争武器" ,西服::钻石=> "这项艺术的钱" ,默认=> "我的心的形状" ,}

此用法不需要​​更改匹配。这是当前功能的自然含义。

作为对象,枚举大小写不能用作数组中的键。但是,它们可以用作WeakMap中的键。因为它们是单例,所以它们永远不会被垃圾收集,因此也永远不会从WeakMap中删除。结果是,如果需要,WeakMap可以用作从枚举实例到其他值的可靠映射。

这种用法不需要​​修改WeakMap。这是当前功能的自然含义。

使用RefelctionEnum类可扩展枚举,该类扩展了ReflectionClass。不相关的方法(例如清单属性)被存根以返回空值。它还包含以下其他方法:

hasCase(string $ name):bool-如果存在用该名称定义的Case,则返回true。 例如,$ r-> hasCase(' Hearts')返回true。 getCase(string $ name):ReflectionCase-返回对应大小写的单个ReflectionCase对象。 如果未找到,则抛出ReflectionException。 hasType():bool-如果Enum具有基本等效类型,则返回true。 否则为假。 getType():?string-返回Enum的原始等效类型(如果有的话)(字符串int,string或float)。 如果没有,则返回null。 ReflectionCase代表枚举中的单个Case。 它还扩展了ReflectionClass,并列出了不相关的方法。 它还具有以下方法: getPrimitive():?int | string | float-返回为案例定义的原始等效值(如果已定义)。 如果未定义,则返回null。 getInstance():枚举-返回Case的单例实例,就好像是从Enum中读取的一样。

枚举SortOrder {case ASC;案例DESC; }函数查询($ fields,$ filter,SortOrder $ order){...}

现在,在知道$ order被保证为SortOrder :: ASC或SortOrder :: DESC的情况下,查询功能现在可以安全进行。任何其他值都将导致TypeError,因此不需要进一步的错误检查或测试。

枚举UserStatus:字符串{case Pending =' pending' {公共功能标签():字符串{return' Pending' ; }} case Active =' active' {公共功能标签():字符串{return' Active' ; }} case Suspended =' suspended' {公共功能标签():字符串{return' Suspended' ; }} case CanceledByUser ='取消'# {公共功能标签():字符串{返回'由用户取消' ; }}}

在此示例中,用户的状态可以是UserStatus :: Pending,UserStatus :: Active,UserStatus :: Suspended或UserStatus :: CanceledByUser之一,并且排他地是UserStatus :: Pending,UserStatus :: Suspended或UserStatus :: CanceledByUser。函数可以针对UserStatus键入参数,然后仅接受这四个值,即period。

这四个值都有一个多态的label()方法,该方法返回人类可读的字符串。该字符串独立于“机器名称”原始等效字符串,该字符串可用于例如数据库字段或HTML选择框。

枚举UserStatus:字符串{case Pending =' pending' ;案例有效='有效' ;暂停的案例=暂停的' ;情况CanceledByUser =已取消' ;公共功能标签():字符串{返回匹配项($ this){UserStatus :: Pending => '待定,UserStatus :: Active => '有效' ,UserStatus ::暂停=> '暂停' ,UserStatus :: CanceledByUser => '被用户取消,}; }}

哪种方法更好,将取决于该方法的具体细节,并由开发人员自行决定。

枚举OvenStatus {́ case Off {公共功能turnOn(){return OvenStatus :: On; }} case On {公共功能turnOff(){返回OvenStatus :: Off; }公共函数idle(){返回OvenStatus :: Idle; }} case空闲{公共函数on(){return OvenStatus :: On; }}}

在此示例中,烤箱可以处于三种状态(“关闭”,“打开”和“空转”)中的一种,这表示火焰没有打开,但是在检测到需要时会重新打开。但是,它永远不能从“关闭”变为“空闲”或从“空闲”变为“关闭”;它必须先经过“开启”状态。这意味着无需编写测试,也无需为从“关闭”到“空闲”定义任何代码路径,因为从字面上甚至无法描述该状态。

“枚举”成为语言关键字,通常具有与现有全局常量命名冲突的潜力。

在简单的情况下,有可能允许一起定义多个情况,如下所示:

这仅适用于没有定义方法的简单,非原始支持的情况。由于目前尚不清楚在实践中将有多普遍,因此分组语法有一个有争议的历史,并且很容易在以后添加(如果需要),现在我们省略了该缩写。

因为它们是对象,所以枚举大小写不能用作关联数组中的键。将来可能会对此提供支持,但是目前尚无法解决。目前,WeakMaps已经足够了。