Scala中的枚举Enumeration

总览

本文将会对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
}

代码的解释

  1. 第一行是声明一个枚举名称为Color。
  2. 第二行是声明了一个类型别名。和这个枚举名字相同,其实并不是必须的,也可以定义成其他名称。但是为了代码的可读性,一般来说定义成相同的。
  3. 第三行声明了这个枚举的各枚举项
  4. 第四行结束

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的机制,自己实现了简单的枚举。

你可能感兴趣的:(Scala中的枚举Enumeration)