第 4 回: Lua のオブジェクト指向について紹介する

プロトタイプのオブジェクト指向

Lua には, 明確なクラスの定義が存在しない. JavaScript に似たプロトタイプベースのオブジェクト指向を利用する.

それも, テーブル型とメタテーブルを利用する.

以下は, テーブルコンストラクタを利用して, Dog オブジェクトを作成し, そして, Dog オブジェクトの showProfile メソッドを呼び出す.

obj1.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- obj1.lua
-- オブジェクトを作成
Dog={
   name = "feifei",
   age = 28,
   showProfile = function(self)
		    -- self["name"] = self.name
		    -- self["age"] = self.age
		    prof = string.format("name = %s, age = %d", self.name, self.age)
		    print(prof)
		 end
}

-- showProfile メソッドを呼ぶ
Dog:showProfile()

obj1.lua の実行結果は:

[wtopia lua.hajime]$ lua obj1.lua
name = feifei, age = 28

Lua では, テーブル型の値に対して, table[“key”] と書くところを [table.key] と書くことができるので, オブジェクトファイル指向風に表現することができる.

そして, table:key() のように書くと table.key(table) と同じ意味になる.

これは, かなり面白い仕組みで, 第一引数で, 自身 (self) をとるような関数を定義しておけば, インスタンスのメソッドを呼び出すようにして関数を実行することができる.

しかし, Lua には, new 演算子が用意されていない. そのため, new と同じような動作を行うメソッドを用意する必要がある.

以下のプログラムでは, new メソッドの中で, テーブル型の変数 obj を生成し, そこに, 連想配列のキー name と age と showProfile を設定し, 戻り値として戻している.

obj2.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- obj2.lua
-- Dog クラスを定義
Dog = {}
Dog.new = function(name, age)
	     local obj = {}
	     obj.name = name
	     obj.age = age
	     obj.showProfile = function(self)
				  s = string.format("[name=%s, age=%d]", self.name, self.age)
				  print(s)
			       end
	     return obj
	  end
-- インスタンスを生成
ff = Dog.new("feifei", 28)
yy = Dog.new("yangyang", 8)
yg = Dog.new("yangguang", 2)
kk = Dog.new("kankan", 1)

-- メソッドを呼ぶ
ff:showProfile()
yy:showProfile()
yg:showProfile()
kk:showProfile()

obj2.lua の実行結果は:

[wtopia lua.hajime]$ lua obj2.lua
[name=feifei, age=28]
[name=yangyang, age=8]
[name=yangguang, age=2]
[name=kankan, age=1]

オブジェクト指向の継承を利用する

Lua には, オブジェクト指向の継承を実現するための仕組みが用意されている. なお, ここでは, オブジェクト指向の用語に合わせるように, 便宜的にテーブルのことを, オブジェクトと呼び, テーブルの連想配列の要素をプロパティと呼ぶ.

継承を実現するために, メタテーブルには __index というキーワードが用意されている.

メタテーブルの __index にスーパークラス (継承元クラス) を指定することで, 継承を実現できるようになる.

この仕組みは, __index にスーパークラスのオブジェクトを設定すると, そのオブジェクト自身のキーを探した後, スーパークラスのオブジェクトのプロパティを探索するようになる.

以下は, クラスの継承を実現したもの. Animal クラスを定義し, Animal を継承した, Dog クラスを作った例.

inheri.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
-- inheri.lua
-- 基本となる Animal クラスを定義
Animal = {}
Animal.name = "unknown"
Animal.showProfile = function(self)
			print("*** profile")
			print("name="..self.name)
		     end

-- Animal を継承して Dog クラスを作る
Dog = {}
Dog.new = function(name)
	     local obj = {}
	     obj.name = name
	     setmetatable(obj, {__index=Animal})
	     return obj
	  end

-- インスタンスを生成する
ff = Dog.new("FeiFei")
ff:showProfile()

inheri.lua の実行結果は:

[wtopia lua.hajime]$ lua inheri.lua
*** profile
name=FeiFei

ただし, すべてのクラスに, new メソッドを定義する場合, サブクラスで, スーパークラスの new メソッドを呼ぶことで, スーパークラスのプロパティをすべてもったオブジェクトが作成されることになるので, メタテーブルの __index を操作する必要はないのかもしれない.

以下, メタテーブルを利用しないで, 継承を再現する例.

inheri2.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- inheri2.lua
-- without metatable
-- 基本となる Animal クラスを定義
Animal = {}
Animal.new = function(name)
		local obj = {}
		obj.name = name
		obj.enumProperty = function(self)
				      print("**" .. self.name)
				      for key, val in pairs(self) do
					 print("-" .. key, val)
				      end
				   end
		return obj
	     end

-- Animal を継承して Dog クラスを作る
Dog = {}
Dog.new = function(name)
	     local obj = Animal.new(name)
	     obj.bark = function(self)
			   print(self.name .. ":bowwow!")
			end
	     return obj
	  end
-- Dog を継承して BullDog クラスを作る
BullDog = {}
BullDog.new = function(name)
		 local obj = Dog.new(name)
		 obj.walk = function(self)
			       print(self.name .. ":walking")
			    end
		 return obj
	      end
-- インスタンスを生成する
jiro = Dog.new("jiro")
jiro:bark()
jiro:enumProperty()

sabu = BullDog.new("sabu")
sabu:walk()
sabu:enumProperty()

inheri2.lua の実行結果は:

[wtopia lua.hajime]$ lua inheri2.lua
jiro:bowwow!
**jiro
-enumProperty function: 0x100107f00
-name jiro
-bark function: 0x100107ed0
sabu:walking
**sabu
-bark function: 0x1001081a0
-enumProperty function: 0x1001081d0
-name sabu
-walk function: 0x1001082a0

Lua のオブジェクト指向は, 様々な実現方法がある. 好きな方法を利用してオブジェクト指向を実践すると良いでしょう

Lua Wiki

関数についての補足

関数の定義の際にも, [:] を使うことができる. 以下の三つの関数の定義はどれも同じ意味になる:

-- obj["a"] に関数を設定する
obj.a = function(self) print(self) end

-- 上と同じ意味
function obj.a(self) print(self) end

-- 上と同じ意味
function obj:a() print(self) end

関数を定義する際にも, self を省略できるのは便利.

モジュールについて

少し規模の大きなプログラムを作る場合, プログラムのモジュールが必須となる.

Lua でもモジュール化の機能が提供されている.

modulu_add.lua

1
2
3
4
-- modulu_add.lua
function add(a, b)
   print("add(a, b) = " .. a+b)
end

modulu_main.lua

1
2
3
-- modulu_main.lua
require("modulu_add")
add(10, 20)

modulu_main.lua の実行結果は:

[wtopia lua.hajime]$ lua modulu_main.lua
add(a, b) = 30

ライブラリを完全にモジュール化するには, module 宣言を利用する. モジュール化することで名前の衝突を防ぐことができる.

ライブラリを利用するには, [ライブラリ名.変数名] のように利用することができる.

test.lua

1
2
3
4
5
6
-- test.lua
module("test", package.seeall)

function hoge()
   print("hoge")
end

main.lua

1
2
3
-- main.lua
require("test")
test.hoge()

main.lua の実行結果は:

[wtopia lua.hajime]$ lua main.lua
hoge