プログラミングの魔物

エラー、バグ、仕様変更と戦うブログ

Luaで列挙型(enum)

Luaではグローバル変数の宣言が必要ないため、スペルミスしてもすぐには気づけない。
たとえ変数名が間違っていたとしても、nil値として判定されてしまうからだ。

以前にグローバル変数で定数を定義した時に、綴りを間違えてしまいバグが発生したことがある。
その際はグローバル変数へのアクセスを監視することで、バグの位置を特定した。

単純なスペルミスであれば、メタテーブルに「__index」、「__newindex」を設定することで発見できる。
これにより大抵のミスは防げるが、そもそも定数をグローバル変数で管理すること自体あまり良いことではない。

やはり定数は定数として管理できる機構が欲しい。
というわけで、Luaで列挙型のクラスを書いた。

列挙型クラス(enum.lua)

local Enum = {}
local lenKey = "len"
local _ = {}
setmetatable(_, {__mode = "k"})

function Enum:__len()
	local i = 0
	for k, v in pairs(_[self]) do
		if k ~= lenKey then
			i = i + 1
		end
	end
	return i
end

function Enum:new(o)
	local t = {}
	_[t] = {[lenKey] = Enum.__len}
	for k, v in pairs(o) do
		if tonumber(k) ~= nil then
			k, v = v, k - 1
		end
		if _[t][k] ~= nil then
			error('"' .. k .. '" can not be set.', 2)
		end
		_[t][k] = v
	end
	return setmetatable(t, self)
end

function Enum:__index(k)
	if _[self][k] == nil then
		error('"' .. k .. '" is undefined enumerator.', 2)
	end
	return _[self][k]
end

function Enum:__newindex()
	error("Enum is read-only.", 2)
end

return Enum

メイン

Enum = require "enum"

test = Enum:new{"aa", "bb", cc = 100}

print(test.aa)  --> 0
print(test.bb)  --> 1
print(test.cc)  --> 100
print(#test)  --> 0 (Lua5.1) or 3 (Lua5.2)
print(test:len())  --> 3
--print(test.dd)  --> error : "dd" is undefined enumerator.
--test.aa = 10  --> error : Enum is read-only.

宣言されていない列挙子を読み込もうとするとエラーになる。かつ、読み取り専用(rawsetは可能だがしない方がいい)
C言語enumと違って要素の値は数値に限らない。また、初期化の中で値が指定されても次の要素をインクリメントしない。
テーブル型において__lenが優先されるのはLua5.2から。Lua5.1では配列以外は0が返る。そのため代替として「len()」メソッドを用意している。

メモ

  • 作っただけでまだ利用はしていないので、これからまた修正するかもしれない(既に一度修正済み)
  • C言語と連携するならそちらで定義すべきかもしれない。
  • そもそもポリモーフィズムで列挙型を置き換えればこのクラス自体必要なくなるかも知れない。