Lua メタテーブル(Metatable)

Lua のテーブルでは、対応するキーにアクセスして値を取得できますが、2つのテーブルに対して演算(例えば加算)を行うことはできません。

そこで Lua はメタテーブル(Metatable)を提供し、テーブルの動作を変更できるようにしています。各動作には対応するメタメソッドが関連付けられています。

例えば、メタテーブルを使用すると、Lua が2つのテーブルの加算操作 a+b をどのように計算するかを定義できます。

Lua が2つのテーブルを加算しようとするとき、まずどちらかにメタテーブルがあるかを確認し、次に「__add__index というフィールドがあるかどうかを確認します。見つかった場合は、対応する値を呼び出します。__addこのようなフィールドに対応する値(多くの場合、関数またはテーブル)が「メタメソッド」です。

メタテーブルを扱うための非常に重要な関数が2つあります:

  • setmetatable(table,metatable):指定されたテーブルにメタテーブルを設定します。メタテーブルの中に __metatable キーが存在する場合、setmetatable は失敗します。
  • getmetatable(table):オブジェクトのメタテーブルを返します。

次の例は、指定したテーブルにメタテーブルを設定する方法を示しています:

mytable = {}                          -- 普通のテーブル
mymetatable = {}                      -- メタテーブル
setmetatable(mytable,mymetatable)     -- mymetatable を mytable のメタテーブルに設定

上記のコードは1行で書くこともできます:

mytable = setmetatable({},{})

以下はオブジェクトのメタテーブルを返します:

getmetatable(mytable)                 -- 这会返回 mymetatable

__index メタメソッド

これはメタテーブルで最もよく使われるキーです。

キーを使ってテーブルにアクセスするとき、そのキーに値がない場合、Lua はそのテーブルのメタテーブル(メタテーブルがある場合)にある __index キーを探します。__index がテーブルを含む場合、Lua はそのテーブル内で対応するキーを探します。

lua コマンドで対話モードに入り、次のように確認できます:

$ lua
Lua 5.3.0  Copyright (C) 1994-2015 Lua.org, PUC-Rio
> other = { foo = 3 }
> t = setmetatable({}, { __index = other })
> t.foo
3
> t.bar
nil

__index が関数を含む場合、Lua はその関数を呼び出します。テーブルとキーが関数の引数として渡されます。

__index メタメソッドは、テーブル内に要素が存在するかどうかを確認します。存在しない場合は nil を返し、存在する場合は __index が結果を返します。

例

mytable = setmetatable({key1 = "value1"}, {
  __index = function(mytable, key)
    if key == "key2" then
      return "metatablevalue"
    else
      return nil
    end
  end
})

print(mytable.key1,mytable.key2)

例の出力結果は次のとおりです:

value1    metatablevalue

例の解説:

  • mytable テーブルに代入:{key1 = "value1"}。

  • mytable にメタテーブルが設定され、メタメソッドは __index です。

  • mytable テーブル内で key1 を探し、見つかった場合はその要素を返し、見つからない場合は続行します。

  • mytable テーブル内で key2 を探し、見つかった場合は metatablevalue を返し、見つからない場合は続行します。

  • メタテーブルに __index メソッドがあるかどうかを判定し、__index メソッドが関数であればその関数を呼び出します。

  • メタメソッド内で "key2" キーの引数が渡されたかどうかを確認します(mytable.key2 は設定済み)。"key2" 引数が渡された場合は "metatablevalue" を返し、それ以外の場合は mytable の対応するキーの値を返します。

上記のコードは次のように簡潔に書くこともできます:

mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)

まとめ

Lua がテーブルの要素を探すときの規則は、実に次の3つのステップです:

  • 1. テーブル内で探します。見つかった場合はその要素を返し、見つからない場合は続行します。
  • 2. そのテーブルにメタテーブルがあるかどうかを判定します。メタテーブルがない場合は nil を返し、ある場合は続行します。
  • 3. メタテーブルに __index メソッドがあるかどうかを判定します。__index メソッドが nil の場合は nil を返し、__index メソッドがテーブルの場合は 1、2、3 を繰り返し、__index メソッドが関数の場合はその関数の戻り値を返します。

この部分の内容は作者の寰子によるものです:https://blog.csdn.net/xocoder/article/details/9028347


__newindex メタメソッド

__newindex メタメソッドはテーブルの更新に使われ、__index はテーブルのアクセスに使われます。

テーブルの存在しないインデックスに値を代入しようとすると、インタープリタは __newindex メタメソッドを探します。存在する場合はその関数を呼び出し、代入操作は行いません。

次の例は __newindex メタメソッドの適用を示しています:

例

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)

mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)

mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)

上記の例を実行した出力結果は次のとおりです:

value1
nil    新值2
新值1    nil

上記の例では、テーブルにメタメソッド __newindex が設定されており、新しいインデックスキー(newkey)に値を代入するとき(mytable.newkey = "新值2")、メタメソッドが呼び出され、代入は行われません。一方、既存のインデックスキー(key1)に代入する場合は、代入が行われ、メタメソッド __newindex は呼び出されません。

次の例では rawset 関数を使ってテーブルを更新しています:

例

mytable = setmetatable({key1 = "value1"}, {
    __newindex = function(mytable, key, value)
        rawset(mytable, key, "\""..value.."\"")
    end
})

mytable.key1 = "new value"
mytable.key2 = 4

print(mytable.key1,mytable.key2)

上記の例を実行した出力結果は次のとおりです:

new value    "4"

テーブルに演算子を追加する

次の例は2つのテーブルの加算操作を示しています:

例

-- テーブルの最大キー値を計算する独自関数 table_maxn、つまりテーブルの最大キー値を返す
function table_maxn(t)
    local mn = 0
    for k, _ in pairs(t) do
        if type(k) == "number" and k > mn then
            mn = k
        end
    end
    return mn
end

-- 2つのテーブルの加算操作
mytable = setmetatable({ 1, 2, 3 }, {
  __add = function(mytable, newtable)
    local max_key_mytable = table_maxn(mytable)
    for i = 1, table_maxn(newtable) do
      table.insert(mytable, max_key_mytable + i, newtable[i])
    end
    return mytable
  end
})

secondtable = {4, 5, 6}

mytable = mytable + secondtable

for k, v in ipairs(mytable) do
    print(k, v)
end

上記の例を実行した出力結果は次のとおりです:

1    1
2    2
3    3
4    4
5    5
6    6

__add キーはメタテーブルに含まれ、加算操作が行われます。テーブル内の対応する演算の一覧は次のとおりです:(注意:__アンダースコア2つです)

パターン説明
__add対応する演算子 '+'.
__sub対応する演算子 '-'.
__mul対応する演算子 '*'.
__div対応する演算子 '/'.
__mod対応する演算子 '%'.
__unm対応する演算子 '-'.
__concat対応する演算子 '..'.
__eq対応する演算子 '=='.
__lt対応する演算子 '<'.
__le対応する演算子 '<='.

__call メタメソッド

__call メタメソッドは、Lua が値を呼び出すときに呼び出されます。次の例は、テーブル内の要素の合計を計算する方法を示しています:

例

-- テーブル内の最大値を計算する。table.maxn は Lua 5.2 以降では使用できません
-- テーブルの最大キー値を計算する独自関数 table_maxn、つまりテーブルの要素数を計算する
function table_maxn(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- メタメソッド __call を定義
mytable = setmetatable({10}, {
  __call = function(mytable, newtable)
        sum = 0
        for i = 1, table_maxn(mytable) do
                sum = sum + mytable[i]
        end
    for i = 1, table_maxn(newtable) do
                sum = sum + newtable[i]
        end
        return sum
  end
})
newtable = {10,20,30}
print(mytable(newtable))

上記の例を実行した出力結果は次のとおりです:

70

__tostring メタメソッド

__tostring メタメソッドは、テーブルの出力動作を変更するために使用します。次の例では、テーブルの出力内容をカスタマイズしています:

例

mytable = setmetatable({ 10, 20, 30 }, {
  __tostring = function(mytable)
    sum = 0
    for k, v in pairs(mytable) do
                sum = sum + v
        end
    return "テーブルのすべての要素の合計は " .. sum
  end
})
print(mytable)

上記の例を実行した出力結果は次のとおりです:

表所有元素的和为 60

この記事から、メタテーブルがコードの機能をうまく簡素化できることがわかります。Lua のメタテーブルを理解することで、よりシンプルで優れた Lua コードを書くことができるようになります。

その他の拡張