前回は簡単な例題として連結リストという基本的なデータ構造を作成しました。今回は「継承 (inheritance : インヘリタンス)」について説明します。
クラスベースのオブジェクト指向では、クラスに親子関係を持たせることを「継承」といいます。子クラスは親クラスの性質を受け継ぐことができます。オブジェクト指向言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドなどになります。プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる差分プログラミングが可能になります。
クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。
たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 の bar が実行されるのです。
メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラス Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーになります。
継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これをオーバーライド (over ride) といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。
Lua にクラスはありませんが、あるオブジェクトのインスタンス変数やメソッドを引き継ぐことは簡単に行うことができます。クラスを表すオブジェクトにおいて、メタテーブルのフィールド __index に、スーパークラスを表すオブジェクトを設定するだけです。フィールドの探索はメタテーブルをたどって行われるので、メタテーブルのオブジェクトがスーパークラス的な役割をはたしていると考えることができます。
簡単な例を示しましょう。次のリストを見てください。
リスト 1 : 継承 Foo = {} function Foo.new() return setmetatable({a = 10}, {__index = Foo}) end function Foo:get_a() return self.a end function Foo:set_a(x) self.a = x end Bar = {} -- Foo を継承 setmetatable(Bar, {__index = Foo}) function Bar.new() local obj = Foo.new() obj.b = 20 return setmetatable(obj, {__index = Bar}) end function Bar:get_b() return self.b end function Bar:set_b(x) self.b = x end
継承のポイントは Bar のメタテーブルのフィールド __index に Foo を指定するところです。そして、Bar.new では Foo.new で生成したインスタンスに Bar で使用するフィールドを追加し、メタテーブルには __index = Bar を設定します。
フィールドの探索はインスタンスから行われます。ここでフィールドが見つからない場合、メタテーブルの __index に指定されているテーブルを探索します。この場合、__index には Bar が指定されているので Bar を探索します。ここでもフィールドが見つからない場合、今度は Bar のメタテーブルをチェックします。ここで __index に Foo が指定されていれば Foo を探索するのです。これで Foo のメソッドを継承することができます。
インスタンス変数の継承も簡単です。スーパークラス Foo のインスタンス obj を生成し、そこに自分のインスタンス変数を追加するだけです。そして、setmetatable で obj のメタテーブルを {__index = Bar} に書き換えればいいわけです。
それでは実際に試してみましょう。
> x = Foo.new() > x table: 0x7fffcb18c710 > x:get_a() 10 > y = Bar.new() > y table: 0x7fffcb18f950 > y:get_b() 20 > y:get_a() 10 > x:set_a(100) > x:get_a() 100 > y:set_a(1000) > y:get_a() 1000 > x:get_a() 100
変数 x, y に Foo と Bar のインスタンスをセットします。インスタンスを生成するとき、Foo のインスタンスにはフィールド a に 10 がセットされ、Bar のインスタンスにはフィールド a, b に 10, 20 がセットされます。
x:get_a() はフィールド a の値を返すので 10 になります。y:get_a() は Foo のメソッド get_a が呼び出され、インスタンス y からフィールド a の値を取り出して 10 を返します。x:set_a(100) はインスタンス x のフィールド a の値を 100 に書き換えます。y:set_a(1000) は Foo のメソッド set_a を呼び出して、インスタンス y のフィールド a の値を 1000 に書き換えます。
それでは簡単な例題として、連結リストを継承して、格納する要素数を制限する連結リストを作ってみましょう。名前は FixedList としました。プログラムをリスト 2 に示します。
リスト 2 : 制限付き連結リスト -- クラス定義 FixedList = {} -- 継承 setmetatable(FixedList, {__index = List}) -- コンストラクタ function FixedList.new(limit) local obj = List.new() obj.limit = limit obj.size = 0 setmetatable(obj, {__index = FixedList}) return obj end -- オーバーライド -- データの挿入 function FixedList:insert(n, x) if self.size < self.limit then local result = List.insert(self, n, x) if result then self.size = self.size + 1 return result end else return nil end end -- データの削除 function FixedList:remove(n) if self.size > 0 then local result = List.remove(self, n) if result then self.size = self.size - 1 end return result else return nil end end -- 満杯か? function FixedList:isFull() return self.size == self.limit end
制限付き連結リスト (FixedList) は指定した上限までしか要素を格納できません。連結リスト (List) で要素を追加するメソッドは insert で、削除するメソッドは remove です。この 2 つのメソッドをオーバーライドすることで、FixedList の機能を実現することができます。
まずグローバル変数 FixedList に空のハッシュをセットし、setmetatable で FixedList のメタテーブルに {__index = List} を設定します。これで List のメソッドを継承することができます。コンストラクタ FixedList.new では、List.new を呼び出して List のインスタンスを生成して変数 obj にセットします。ここに FixedList で使用するフィールド limit と size を追加します。limit は要素数の上限値を表していて、引数 limit で指定します。size は連結リストに格納されている要素数を表します。
それから、メソッド insert と remove をオーバーライドします。フィールドの探索は、FixedList のインスタンス、FixedList を表すハッシュ、List を表すハッシュの順番で行われます。FixedList にメソッドを追加すれば、List のメソッドをオーバーライドすることができます。
insert は self.limit と self.size を比較して、self.size が self.limit よりも小さい場合はデータ x を挿入します。スーパークラスのメソッド List.insert を呼び出して、データを挿入できた場合は self.size を +1 します。remove の場合、self.size が 0 よりも大きいときにスーパークラスのメソッド List.remove を呼び出します。データを削除できた場合は self.size を -1 します。これで、連結リストに格納される要素数を管理することができます。
それでは、簡単な実行例を示しましょう。
> a = FixedList.new(5) > for i = 1, 5 do a:insert(1, i) end > a:toString() (5,4,3,2,1) > a:isFull() true > a:isEmpty() false > a:insert(1, 10) nil > a:toString() (5,4,3,2,1) > while not a:isEmpty() do print(a:remove(1)) end 5 4 3 2 1 > a:isFull() false > a:isEmpty() true
このように List を継承することで、FixedList を簡単にプログラムすることができます。
-- -- linklist.lua : 連結リスト -- -- Copyright (C) 2011-2019 Makoto Hiroi -- -- セルの定義 Cell = {} -- コンストラクタ function Cell.new(data, link) return {data = data, link = link} end -- リストの定義 List = {} function List.new(...) local obj = {top = Cell.new(nil, nil)} local cp = obj.top setmetatable(obj, {__index = List}) for i = 1, select("#", ...) do local x = select(i, ...) cp.link = Cell.new(x, nil) cp = cp.link end return obj end -- メソッドの定義 -- 作業用メソッド : n 番目のセルを返す function List:_nth(n) local cp = self.top local i = 0 while cp do if n == i then return cp else cp = cp.link i = i + 1 end end return nil end -- n 番目の要素を返す function List:at(n) local cp = self:_nth(n) if cp then return cp.data else return nil end end -- n 番目の要素を書き換える function List:set(n, x) local cp = self:_nth(n) if cp then cp.data = x return x else return nil end end -- n 番目にデータを挿入 function List:insert(n, x) local cp = self:_nth(n - 1) if cp then cp.link = Cell.new(x, cp.link) return x else return nil end end -- n 番目の要素を削除 function List:remove(n) local cp = self:_nth(n - 1) if cp and cp.link then local data = cp.link.data cp.link = cp.link.link return data else return nil end end -- 巡回 function List:foreach(func) local cp = self.top.link while cp do func(cp.data) cp = cp.link end end -- イテレータ function List:each() return coroutine.wrap( function() self:foreach(function(x) coroutine.yield(x) end) return nil end) end -- 空リストか function List:isEmpty() return self.top.link == nil end -- 配列に変換 function List:toArray() local ary = {} self:foreach(function(x) table.insert(ary, x) end) return ary end -- 文字列に変換 function List:toString() return '(' .. table.concat(self:toArray(), ',') .. ')' end -- -- 制限付き連結リスト -- -- クラス定義 FixedList = {} -- 継承 setmetatable(FixedList, {__index = List}) -- コンストラクタ function FixedList.new(limit) local obj = List.new() obj.limit = limit obj.size = 0 setmetatable(obj, {__index = FixedList}) return obj end -- オーバーライド -- データの挿入 function FixedList:insert(n, x) if self.size < self.limit then local result = List.insert(self, n, x) if result then self.size = self.size + 1 return result end else return nil end end -- データの削除 function FixedList:remove(n) if self.size > 0 then local result = List.remove(self, n) if result then self.size = self.size - 1 end return result else return nil end end -- 満杯か? function FixedList:isFull() return self.size == self.limit end