枚举类型在Go语言中的一种写法
2017-03-06
枚举类型在ML类语言里面写起来是最自然的,比如ocaml,F#里面:
type T =
| T1 of int
| T2 of string
| T3 of bool
haskell里面好像用的datetype而不是type,不太记得细节。总之这是非常重要的一种类型,并不仅限于函数式语言中。
在C语言里面,没有类型安全的保证。好在C里面有union类型,并且有指针,指针这东西太灵活了,能玩得飞起。在C里面枚举类型可以这样写:
struct T {
int kind;
union {
char* T1;
int T2;
bool T3;
} value;
};
这里有个小的细节,union分配的大小等于结构体里面最大的那个,所以这种写法不一定最优。C足够灵活,就不多说了。
在Go里面,连union关键字都没有了,类型也做的更严格,指针不像C那样可以随意转。可能比较标准的做法是用interface。interface也是可以玩得飞起的,比如这篇文章以前探索过。但是现在回头想想,觉得太复杂。
interface{}像C的指针一样,它可以是任何类型。但是有些问题吧,第一个问题,它的类型安全是靠运行时判断的,相比函数式语言只产生编译期开销,还逊色不少。
另一个问题是对代码洁癖者说的,其实只要代码里出写:
if _, ok := t.(T); ok {}
我认为基本算得上bad smell了。Go语言讲究依赖于接口而不是实现(其实原则上任何语言都是),你都使用接口了,还需要反射出具体的实现来,那就是丑。
再一个重要的问题当然是性能了。参数传递会有对象分配,用interface反射出原始值也会有对象分配。即无论你是
func f(v interface{}) {}
f(3) // 由于参数是interface,这里会分配一个interface对象,对象的值指向3
还是
raw, ok := t.(T) // raw不是原始值,而是分配一个T并从原始值拷贝。
都是有内存分配开销的。在一些性能重要的场合,这种开销对GC还是危害挺大的。
反正觉得在Go里面用interface实现枚举类型不算是优雅的方案。
下面就是我觉得更好的一种写法了。
先定义嵌入类:
type T struct {
kind enumT
}
在实现要枚举的T1 T2 T3,都嵌入这个类型:
type T1 struct {
T
v int
}
type T2 struct {
T
v string
}
type T3 struct {
T
v bool
}
实际使用的时候,我们的类型都是*T
,判断kind去获知真实类型:
var t *T
switch t.kind {
case kindT1:
case kindT2:
default:
}
创建牧举类型的对象的写法:
var t *T
tmp := T1{}
tmp.kind = kindT1
t = &tmp.T
当然最好是封装成函数
func createT1() *T
func createT2() *T
func createT3() *T
可以为kind的类型做一个别名,并定义常量:
type enumT int
const (
kindT1 enumT = iota
kindT2
kindT3
)
用枚举 *T
反射出真实的类型 *T1
需要用到指针转换,用unsafe,其实也可以用函数封起来:
var t T
raw := (*T1)(unsafe.Pointer(t))
相比于用interface,这种写法是跟C的指针一样无开销,并且对于GC也是安全的(用unsafe.Pointer存东西是不安全的)。
有疑问加站长微信联系(非本文作者)