总览
本文将会对scala中的枚举类型实现进行深入的探讨,并且和其他语言中的枚举做简单的比较。
什么是枚举
现实中的事物往往只有 有限 个个体组成,枚举就是为了表示本事物的个体的有限个枚举项。参照《百科》对枚举的介绍:
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数
比如表示一个问题的对错、真假等的bool类型,就是一个枚举。
enum Bool {
TRUE, FALSE
}
再比如人的眼睛只能看到3种颜色:红、绿、蓝。颜色就是一个枚举。其他颜色都是这三种颜色的不同比例的混合。
enum Color {
RED, GREEN, BLUE
}
比如一周7天的名称就是一个枚举
enum WeekDay {
Mon, Tue, Wed, Thu, Fri, Sat, Sun
}
编程语言中的枚举项一般来说都有一个编号(一般是自动编号)和一个名称(可选)。编号一般用于比较,名称一般用于引用这个枚举项。
枚举项有如下一些隐含的特性
1 枚举项被定义之后是不能被修改的,也就是一些语言中的常量。
2 枚举项是各不相同的。
其他语言中的枚举
在一些语言中引入了enum语法, 可以直接定义枚举。一些语言中没有enum语法(1.5之前的java,python, scala等),为了表示枚举,使用了一些特殊的方法。
比如java在引入enum关键字之前,可以使用私有构造函数+静态字段方式来模拟enum
public class Color {
public static final Color RED = new Color(1);
public static final Color GREEN = new Color(2);
public static final Color BLUE = new Color(3);
private int id;
private Color(int id) {
this.id = id;
}
// override equals & hashCode
}
而1.5版本之后java中的enum也只是语法糖而已,最终编译后的enum也和上面的的代码没有什么区别,只是添加了一些辅助方法。
对于一些有enum语法支持的语言,直接定义就可以。
比如上一节举例的枚举都是c语言风格的枚举。为了实现枚举项的2个特性,基本所有语言中的枚举项都是给每个枚举一个固定而不同的编号,那么所有的枚举就不会相等了。
Scala中的枚举
scala中也没有enum关键字和语法支持,虽然scala有非常复杂的类型系统。scala中的枚举是使用了标准库中的scala.Enumeration来实现的。一般的,声明一个枚举的方法如下
object Color extends Enumeration {
type Color = Value
val RED, GREEN, BLUE = Value
}
代码的解释
- 第一行是声明一个枚举名称为Color。
- 第二行是声明了一个类型别名。和这个枚举名字相同,其实并不是必须的,也可以定义成其他名称。但是为了代码的可读性,一般来说定义成相同的。
- 第三行声明了这个枚举的各枚举项
- 第四行结束
object Color
中的Color
只是一个普通的对象(Enumeration的一个实现),和其他的对象没有什么太大的区别。它内部定义了一个type Color
别名和3个常量RED
, GREEN
, BLUE
。
为了了解为什么这么声明,我们深入到Enumeration的内部去看一下。
Enumeration内部
Enumeration是scala标准库中的类,是一个抽象(abstract)类,代码很长,把重要部分抽取出来之后的代码如下(为了简化做了稍许修改):
abstract class Enumeration {
thisenum =>
private val vmap: mutable.Map[Int, Value] = new mutable.HashMap
protected var nextId: Int = 0
protected final def Value: Value = Value(nextId)
protected final def Value(i: Int): Value = new Val(i)
abstract class Value extends Ordered[Value] with Serializable {
def id: Int
private[Enumeration] val outerEnum = thisenum
override def equals(other: Any): Boolean = other match {
case that: Enumeration#Value => (outerEnum eq that.outerEnum) && (id == that.id)
case _ => false
}
}
protected class Val(i: Int) extends Value with Serializable {
assert(!vmap.isDefinedAt(i), "Duplicate id: " + i)
vmap(i) = this
nextId = i + 1
def id: Int = i
}
}
注意其中和Value相关的方法def Value
和类 abstract class Value
。
我们定义的枚举中的type别名中的Value就是类,而枚举项中的Value就是方法。
枚举项定义val RED, GREEN, BLUE = Value
要看懂这句,先要明白scala中的一个简单语法,一句话给多个变量赋值,看如下代码
val a, b, c = 1
那么相当于给常量 a
, b
, c
都赋值为1
具体到这里的定义,相当于RED
, GREEN
, BLUE
都赋值给了Value
。而这里的Value就是方法def Value: Value
。只是他的方法名和返回值都叫Value而已。 而方法Value具体的功能,就是返回一个新的Value的实例,并且id每次都自动加1.
那么也就是RED,GREEN,BLUE都是Value类型,并且id一次加1.
RED.id == 0
GREEN.id == 1
BLUE.id == 2
别名定义type Color = Value
枚举项已经生成了,那么怎么使用呢。首先是声明的地方,需要定义个变量(或常量),他的类型是Color,值是枚举值。那么这里的变量别名就派上了用场。
枚举项都是Value类型,但是怎么区分不同的枚举中的枚举项呢?就靠这里的别名。因为定义中的object Color
是一个对象实例,不能用作类型。要是使用Color.Value作为类型,也感觉怪怪的,所以别名定义就是为了方便在使用枚举时的声明。举个例子:
import Color._
def paint(text: String, color: Color)
枚举的比较
枚举大部分情况下用来比较。对于不同的枚举,肯定不会相同,即便他们的id和名称都是相同的也不会。可以看一下Enumeration中的thisenum
这个self type
。 thisenum相当于是this,但是在Value类中使用到。为了区分Value中的this和Enumeration中的this,定义了this的别名也即
thisenum == Enumeration.this
那么对于不同的枚举,他们的thisenum是不同的,所以在Value的equals方法中,就先判断了(outerEnum eq that.outerEnum)
。对于同一个枚举,这两个是一样的,对于不同的枚举,这两个肯定是不相同的(为什么不使用==,参考scala中的==和eq的用法)。
先判断了thisenum是否相同,然后再判断id(id == that.id)
就完成了比较。
扩展:定义自己的枚举
看到这里,我们明白了,其实scala中的枚举只是一个普通的object。那么我们是否可以不使用Enumeration来自己定义个枚举呢?答案是可以的,而且非常简单。
首先定义枚举的父类,当然也是abstract的:
abstract class enum(initial: Int = 0) {
en =>
protected class Item(val id: Int) {
override def toString: String = st.toString + "(" + id + ")"
private[enum] val st = en
override def equals(obj: Any): Boolean = obj match {
case that: Item => (st eq that.eq) && (id == that.id)
case _ => false
}
override def hashCode(): Int = id.##
}
private val nmap = new collection.mutable.HashMap[Int, Item]
var nextId: Int = initial
def Item: Item = createItem(nextId)
def createItem(i: Int): Item = {
val item = new Item(i)
nextId += 1
nmap(i) = item
item
}
def withId(id: Int): Item = nmap(id)
}
定义时仿照Enumeration即可
object Color extends enum {
type Color = Item
val RED, GREEN, BLUE = Item
}
总结
本文对scala和其他语言中的枚举进行了一些简单的介绍,深入scala的Enumeration内部,研究了它的实现机制。之后,参照scala的机制,自己实现了简单的枚举。