M.Hiroi's Home Page

お気楽 Lua プログラミング超入門

Lua でオブジェクト指向


Copyright (C) 2011-2019 Makoto Hiroi
All rights reserved.

はじめに

一般的なオブジェクト指向言語の場合、まず最初に「クラス (class)」を定義し、それを元にして「インスタンス (instance)」を生成します。ここではインスタンスのことを「オブジェクト (object)」と考えてください。これは Smalltalk, C++, Java などポピュラーなオブジェクト指向言語や Ruby, Python などのスクリプト言語でも同じです。このようにクラスを中心としたオブジェクト指向を「クラスベース」といいます。

クラスはオブジェクト指向機能の中心で、クラスがなければ何も始まらないと思われる方もいるでしょう。ところが、クラスが存在しないオブジェクト指向もあります。これを「プロトタイプベース」といいます。もしくは「インスタンスベース」と呼ばれることもあるようです。代表的な言語として Self や JavaScript があります。

プロトタイプベースの言語では、元となるインスタンスをコピーすることで新しいインスタンスを生成します。このコピー元のインスタンスを「プロトタイプ」といいます。そして、新しいインスタンスに必要となる機能 (メソッドなど) を追加します。このインスタンスが新たなプロトタイプとなり、あとはこのプロトタイプを必要な分だけコピーしてインスタンスを生成すればいいわけです。

今回は JavaScript を参考に、Lua で簡単なオブジェクト指向プログラミングに挑戦してみましょう。なお、Lua でオブジェクト指向を実現する場合、いろいろな方法が考えられています。英語ですが lua-users wiki の SampleCode には、オブジェクト指向のサンプルプログラムが掲載されています。興味のある方はお読みくださいませ。

●オブジェクトの生成

Lua でオブジェクト指向を実現する場合、オブジェクトをテーブル (ハッシュ) で表します。これをインスタンスと考えます。キーに対応するハッシュの領域をフィールドと呼ぶことにすると、フィールドにデータを格納すればインスタンス変数になり、関数を格納すればメソッドとして呼び出すことができます。このようなインスタンスを生成する関数 (コンストラクタ) を考えます。次のリストを見てください。

リスト 1 : コンストラクタの定義

Foo = {}

function Foo.new(a, b)
  return {a = a, b = b}
end

グローバル変数 Foo に空のハッシュをセットします。そして、function で Foo.new(a, b) という関数を作ります。この場合、Foo の中に new というフィールドが作成され、そこに関数がセットされます。Foo.new はハッシュ {a = a, b = b} を生成して返すだけです。これがインスタンスになります。

それでは実際にインスタンスを生成してみましょう。

> x = Foo.new(10, 20)
> x
table: 0x7fffd38ba520
> x.a
10
> x.b
20
> y = Foo.new(100, 200)
> y
table: 0x7fffd38bb4b0
> y.a
100
> y.b
200

Lua の場合、ハッシュのアクセス方法は [ ] だけではなく、object.name でもアクセスすることができます。x.a は x["a"] と同じ意味です。ただし、x.1 のように数値を直接指定することはできません。この場合は [ ] を使って x[1] とします。なお、Lua にはアクセスを制限する機能はありません。どこからでもアクセスすることができます。

●メソッドの定義

Lua の場合、インスタンスのフィールドに関数をセットすれば、それをメソッドとして呼び出すことができます。メソッドの呼び出しは object.method(self, ...) になります。このとき、メソッドの第 1 引数 self に object を渡します。method はこの引数を使って呼び出したオブジェクト (object) を参照することができます。次のリストを見てください。

リスト 2 : メソッドの定義 (1)

Foo = {}

function Foo.new(a, b)
  return {
    a = a,
    b = b,
    get_a = function(self) return self.a end,
    get_b = function(self) return self.b end,
    set_a = function(self, x) self.a = x end,
    set_b = function(self, x) self.b = x end
  }
end

コンストラクタ Foo.new の中でメソッド get_a, get_b, set_a, set_b を定義します。匿名関数で関数を生成してフィールドにセットするだけなので簡単です。

それでは実際に試してみましょう。

> x = Foo.new(10, 20)
> x.get_a(x)
10
> x:get_a()
10
> x:set_a(100)
> x:get_a()
100
> x:get_b()
20
> x:set_b(200)
> x:get_b()
200

メソッドを呼び出すとき、object:method() のようにオブジェクトとメソッド名をコロン ( : ) で区切ると、object の値が第 1 引数 self に渡されます。たとえば、x.get_a(x) は x のインスタンス変数 a の値を求めますが、これを x:get_a() と書くことができます。これはとても便利な書式です。

ところで、この方法ではインスタンスを生成するたびに、新たな関数が生成されてインスタンスにセットされます。同じ処理を行う関数をいくつも作るのは無駄ですね。そこで、メソッド用の関数を定義して、それをインスタンスにセットすることにしましょう。次のリストを見てください。

リスト 3 : メソッドの定義 (2)

Foo = {}

function Foo.new(a, b)
  local function get_a(self) return self.a end
  local function get_b(self) return self.b end
  local function set_a(self, x) self.a = x end
  local function set_b(self, x) self.b = x end
  return {
    a = a,
    b = b,
    get_a = get_a,
    get_b = get_b,
    set_a = set_a,
    set_b = set_b
  }
end

Foo.new の中で局所関数 get_a, get_b, set_a, set_b を定義し、それをインスタンスにセットします。これで、無駄な関数の生成を抑えることができます。ただし、この方法でもメソッドは個々のインスタンスに格納されるので、メモリを余分に使うことになります。この問題は「メタテーブル」という機能を使うと解決することができます。

●メタテーブル

Lua はどのデータでも「メタテーブル (meta-table)」を持つことができます。メタテーブルは、そのデータに対して特殊な演算をしたときの挙動を定義するものです。テーブル以外のデータは、その型ごとにひとつのメタテーブルを持っています。このメタテーブルを変更することはできません。

これに対しテーブルの場合は、テーブルごとにメタテーブルを設定することができます。メタテーブルの特定のフィールドに関数やデータを定義することで、そのフィールドに対応する動作を定義することができます。詳細は Lua のリファレンスマニュアルをお読みください。

Lua の場合、テーブルからフィールドを参照するとき、そのテーブルに該当するフィールドが見つからないとエラーになります。ところが、そのテーブルにメタテーブルが設定されていて、さらにメタテーブルの __index フィールドにテーブルがセットされている場合、そのテーブルからフィールドを探索します。そのテーブルでも見つからない場合は同じ処理を繰り返します。つまり、そのテーブルにメタテーブルがあり、さらに __index でテーブルが指定されていれば、__index で指定されたテーブルを探索するわけです。

メタテーブルを使うと、メソッドの情報をインスタンスから他のオブジェクトへ移すことができます。次のリストを見てください。

リスト 4 : メソッドの定義 (3)

-- クラス定義
Foo = {}

-- メソッド定義
function Foo.get_a(self) return self.a end
function Foo.get_b(self) return self.b end
function Foo.set_a(self, x) self.a = x end
function Foo.set_b(self, x) self.b = x end

-- コンストラクタ
function Foo.new(a, b)
  local obj = {a = a, b = b}
  -- メタテーブルセット
  return setmetatable(obj, {__index = Foo})
end

メソッドは Foo のフィールドに格納します。クラスベースのオブジェクト指向に慣れている方ならば、Foo を「クラス」と考えるとわかりやすいかもしれません。そして、Foo.new で生成したインスタンス obj にメタテーブルを設定します。

関数 setmetatable(obj, metatable) は obj のメタテーブルを metatable に設定します。返り値は第 1 引数の obj です。メタテーブルのフィールド __index に Foo を指定すると、obj で見つからないフィールドは、Foo から探索されるようになります。つまり、メソッドは Foo で定義されたものが実行されるわけです。

簡単な実行例を示しましょう。

> Foo
table: 0x7fffb7f94de0
> x = Foo.new(10,20)
> x:get_a()
10
> x:get_b()
20
> x:set_a(100)
> x:get_a()
100
> x:set_b(200)
> x:get_b()
200

Foo.new で生成したインスタンスにはインスタンス変数 a, b しかありませんが、メタテーブルを設定することにより、Foo で定義されているメソッド get_a, get_b, set_a, set_b を呼び出すことができます。

ところで、関数 (メソッド) の定義にコロンを使うと、次のように書き直すことができます。

リスト 5 : メソッド定義 (4)

function Foo:get_a() return self.a end
function Foo:get_b() return self.b end
function Foo:set_a(x) self.a = x end
function Foo:set_b(x) self.b = x end

function foo:bar(...) ... end は function foo.bar(self, ...) ... end に変換されます。これも便利な書式です。

●ポリモーフィズム

一般的なオブジェクト指向の場合、メソッドはオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、ほかの関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar() が定義されていても、クラス Foo2 に同名のメソッド bar() を定義することができます。

そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar() を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar() が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar() が実行されるのです。このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。この機能を「ポリモーフィズム(polymorphism)」と呼びます。これにより、オブジェクトは自分が行うべき適切な処理を実行することができます。

それでは簡単な例題として、点と表すオブジェクトを作ってみましょう。コンストラクタ Point.new で作成するインスタンスは 2 次元座標を表し、Point3D.new で作成するインスタンスは 3 次元座標を表します。それぞれ、2 点間の距離を計算するメソッド distance を定義します。プログラムは次のようになります。

リスト 6 : Point と Point3D

-- 2 次元座標
Point = {}

function Point.new(x, y)
  local obj = {x = x, y = y}
  return setmetatable(obj, {__index = Point})
end

-- 距離を求める
function Point.distance(p1, p2)
  local dx = p1.x - p2.x
  local dy = p1.y - p2.y
  return math.sqrt(dx * dx + dy * dy)
end

-- 3 次元座標
Point3D = {}

function Point3D.new(x, y, z)
  local obj = {x = x, y = y, z = z}
  return setmetatable(obj, {__index = Point3D})
end

-- 距離を求める
function Point3D.distance(p1, p2)
  local dx = p1.x - p2.x
  local dy = p1.y - p2.y
  local dz = p1.z - p2.z
  return math.sqrt(dx * dx + dy * dy + dz * dz)
end

コンストラクタ Point.new, Point3D.new は座標を受け取り、それをインスタンスにセットします。ここで、インスタンスにセットされるメタテーブルは、コンストラクタによって異なることに注意してください。メタテーブルによりインスタンスが属するクラスを設定する、と考えてもらってもかまいません。メソッド distance は引数 p1, p2 にインスタンスを受け取り、p1 と p2 の距離を計算します。math.sqrt は平方根を求める関数です。

それでは実行してみましょう。

> p1 = Point.new(0, 0)
> p1
table: 0x7ffff51c7130
> p2 = Point.new(10, 10)
> p2
table: 0x7ffff51c7a20
> p3 = Point3D.new(0, 0, 0)
> p3
table: 0x7ffff51ca260
> p4 = Point3D.new(10, 10, 10)
> p4
table: 0x7ffff51cacd0
> p1:distance(p2)
14.142135623731
> p3:distance(p4)
17.320508075689

このように、コロンの左側のオブジェクトによって適切なメソッドが呼び出され、ポリモーフィズム (polymorphism) がきちんと働いていることがわかります。

●クラス変数

インスタンス変数は個々のインスタンスに格納される変数です。その値はインスタンスによって変わります。オブジェクト指向言語の中には、クラスで共通の変数や定数を定義する機能があります。これを「クラス変数」といいます。Lua の場合、メタテーブルで指定したハッシュに変数を追加すれば、どのインスタンスからでも参照することができます。

簡単な例を示しましょう。

> Foo = {}
> function Foo.new(a) return setmetatable({a = a}, {__index = Foo}) end
> Foo.bar = 10
> x = Foo.new(1)
> y = Foo.new(2)
> x.a
1
> y.a
2
> x.bar
10
> y.bar
10

Foo.bar がクラス変数になります。Foo.new でインスタンスを生成して、変数 x, y にセットします。x.bar と y.bar は同じフィールド bar を参照するので、同じ値 10 になります。

bar の値を書き換える場合は注意が必要です。次の例を見てください。

> Foo.bar = 20
> x.bar
20
> y.bar
20
> x.bar = 30
> x.bar
30
> y.bar
20
> Foo.bar
20
> x
{a=1,bar=30}

最初の例は Foo.bar の値を書き換えています。この場合、x.bar と y.bar は書き換えた値 20 になります。次に、x.bar = 30 を実行します。このとき、インスタンス x にフィールド bar が存在しないことに注意してください。

メタテーブルのフィールド __index は、テーブルを参照するときの動作を指定するものです。テーブルの値を更新する動作は別のフィールド (__newindex) で指定します。デフォルトの動作は、テーブルにフィールドが存在しない場合、そのテーブルにフィールドを登録し、そこに値を代入します。つまり、インスタンス x にフィールド bar が作られて、そこに 30 がセットされるのです。この場合、Foo.bar は 20 のままなので、y.bar の値は 20 になります。x.bar の値は x のフィールド bar を参照するので 30 になります。

メタテーブルの指定で {__index = Foo, __newindex = Foo} とすると、x.bar = 30 は Foo のフィールド bar の値を書き換えるようになります。ただし、今後のこと (継承など) を考えると、不都合が生じる場合があるので __newindex の指定は行わないものとします。クラス変数は次のようなアクセス関数を用意するといいでしょう。

リスト 7 : クラス変数のアクセスメソッド

Foo = {}

-- コンストラクタ
function Foo.new(a)
  local obj = {a = a}
  return setmetatable(obj, {__index = Foo})
end

-- クラス変数
Foo.bar = 10

-- クラスメソッド
function Foo.show() return Foo.bar end
function Foo.update(x) Foo.bar = x end

-- メソッド
function Foo:get_a() return self.a end
function Foo:set_a(x) self.a = x end

メソッド show で Foo.bar の値を参照し、update で Foo.bar の値を更新します。簡単な例を示しましょう。

> x = Foo.new(1)
> x:get_a()
1
> Foo.show()
10
> Foo.update(100)
> Foo.show()
100

show と update は、クラスベースオブジェクト指向でいうところの「クラスメソッド」という機能に相当します。


初版 2011 年 5 月 1 日
改訂 2019 年 12 月 28 日