M.Hiroi's Home Page

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

継承


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

はじめに

前回は簡単な例題として連結リストという基本的なデータ構造を作成しました。今回は「継承 (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 を簡単にプログラムすることができます。


●プログラムリスト1

--
-- 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

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