Luaでメタテーブルを扱ってみよう!スクリプト解説 Roblox Studio

こんにちは!

あなたは、「メタテーブル」という言葉を聞いたことはありますか?

「メタテーブル、聞いたことはあるし、便利だけど、なんだか難しそう…。」

「海外の解説動画を見たけど、難しくて挫折した…。」

そんな方がこの記事にたどり着いたのではないでしょうか。

この記事では、メタテーブルの基本的な使い方を、たくさんのコードサンプルと共に解説します!ぜひ役立ててくださいね。

この記事の対象者
  • 中学生以上もしくは中学生レベルの国語力がある
  • メタテーブルを除くLuaの基本についてある程度理解している
  • 特に、「メタテーブルってなに?」って人にオススメ!

それでは、始めていきましょう!

メタテーブルとは?

そもそも、メタテーブルの「メタ」とは何なのでしょうか?

メタには「高い次元の」や「超越した」という意味があります。

メタテーブルも、超簡単に言えば、通常のテーブルの上位互換。メタテーブルを使うことで、テーブルの動作のカスタマイズをすることができます。

例えば…。テーブルってふつうは足し算できませんよね。

以下のようなコードがあるとします。

local table1 = {1, 2, 3}
print(table1 + 1)

このコードは、次のようなエラーがでます。

  22:48:29.093  ServerScriptService.Script:2: attempt to perform arithmetic (add) on table and number  -  サーバー - Script:2
  22:48:29.093  Stack Begin  -  Studio
  22:48:29.093  Script 'ServerScriptService.Script', Line 2  -  Studio - Script:2
  22:48:29.093  Stack End  

「テーブルと数値で算術 (加法)を実行しようとした」というエラーがでました。

要するに、「テーブルと数字で足し算できないよ!」ってエラーですね。

普通のテーブルではこうなります。しかし、メタテーブルを活用すれば、テーブルと数値、はたまたテーブルとテーブルで足し算したりすることができるのです。

メタテーブルを使うことで、足し算の演算子(+)だけでなく、他の算術演算子(-, *, /など)や、比較演算子(==, <=など)の演算子の動作を設定することも可能です。

テーブルと数値、テーブルとテーブルを比較演算子で比較して、trueやfalseを返す、といったこともできるんです!

これで、メタテーブルで実現可能なことの5割ぐらいを説明することができました!まだできることはありますが、後ほど紹介します。

それでは早速、メタテーブルを扱ってみましょう!

メタテーブルの基本を学ぼう

この章では、メタテーブルの「基本」の習得を目指していきます!

「メタテーブルを使えば、テーブルで算術演算子が使える!!」といっても、そう簡単ではありません…。

そもそも、テーブルと数値が足し算できないのは、テーブルと数値が足し算するための仕組みが備わっていないからなのです。

メタテーブルを使って、テーブルを足し算できるようにする場合、「どのようにテーブルと演算子の後ろの値を使って、計算するか」を定義する必要があります。

それを踏まえて、メタテーブルを扱っていきましょう。

サンプルコードを実行してみる

では、ここでは、「数値が数個入ったテーブル」にメタテーブルを設定し、数値を足すと、テーブル内の全要素がその数値で足し算されるというものを作ってみます。

これをコードにすると、このような感じです。

-- テーブルの定義
local table1 = {6, 4, 9, 8}

-- 計算
local result = table1 + 2

-- 印刷
print(result) --目指す出力:{8, 6, 11, 10}

先ほど検証したように、通常のテーブルだと、このコードはエラーが出るので、メタテーブルを設定する必要があります。

メタテーブルを使い、「目指す出力」を実現したサンプルコードがこちらです。

--テーブルの定義
local table1 = {6, 4, 9, 8}

-- メタテーブルの定義
local mt = {
    __add = function(t, val)
        local result = {}
        for i, v in ipairs(t) do
            result[i] = v + val
        end
        return result
    end
}

-- table1にメタテーブルを設定
setmetatable(table1, mt)

-- 計算
local result = table1 + 2

-- 印刷
print(result)

コードの内容が理解できなくてもいいので、とにかくコピーして実行してみましょう。

出力結果はこのようになるはずです!先ほどの「期待する出力」どうりになっていますね!

もちろん、演算子の後の数字(2)を変更することもできます。試しに、100にしてみると…

100ずつ足されました!

メタテーブルの基本中の基本を理解する

実際にメタテーブルを使うことができたら、コードの内容について解説していきます。

まずは、この部分。

-- table1にメタテーブルを設定
setmetatable(table1, mt)

テーブルにメタテーブルを設定するには、setmetatable()という特別な関数を使用します。

関数の第一引数で、メタテーブルを設定するテーブルを指定します。
そして、第二引数で、設定するメタテーブルを指定します。

メタテーブルが定義されているのは、この部分ですね。

-- メタテーブルの定義
local mt = {
    __add = function(t, val)
        local result = {}
        for i, v in ipairs(t) do
            result[i] = v + val
        end
        return result
    end
}
mtとは?

ここでのメタテーブルの名前である「mt」とは、「metatable」の略です。自分でメタテーブルを定義するときは、分かりやすい名前にしましょう!

このコードでは、mtというテーブル(メタテーブル)の配下に、__addというキーが入っており、値として関数が格納されている構造になっていることがわかります。

このとき、メタテーブルの配下にある__addのようなキーを、メタメソッドと呼びます。

この__addというメタメソッドは、テーブルに対して + 演算子を使用した際に、メタメソッドの値となっている関数を呼び出すものとなっています。

メタメソッドについて

__add だけでなく、-演算子を使用した際に呼び出される__sub* 演算子を使用した際に呼び出される__mul など、たくさんのメタメソッドがあります。また、演算子関連以外のメタメソッドもあるので、後ほどご紹介します。

では、__addメタメソッドの値になっている関数にフォーカスをあててみましょう。

function(t, val)
   local result = {}
   for i, v in ipairs(t) do
       result[i] = v + val
   end
   return result
end

__addメソッドでは、該当メタテーブルが設定されているテーブルに対して+演算子が使用されたときに、関数を実行するというものですが、関数が実行されたときに、関数に2つの引数が渡されます。

function(t, val)

第一引数のtは、関数がトリガーされることにつながった、該当のテーブルです。
先ほどの「table1 + 2」の「table1」の部分ですね。

反対に、第二引数の val は、演算子の右にあたる値になります。「table1 + 2」の「2」にあたる部分ですね。

第一引数がテーブル、第二引数が演算子の右の値。

これさえ抑えたらあとはかんたんですね。そのテーブルや値を元に結果を計算します。

それが、この部分です。

        local result = {}
        for i, v in ipairs(t) do
            result[i] = v + val
        end

for文を使い、テーブルの要素ごとに指定の数値を足して、resultテーブルに格納しています。

最後に、計算結果であるresultテーブルをreturnして、完了です。

        return result

これで、table1 + 2 が、関数内で計算されたresultテーブルになるわけですね!

これで、メタテーブルの基本中の基本はマスターできましたね!おつかれさまでした~🎉

メタテーブルでメタテーブルを設定する

このセクションでは、メタテーブルでメタテーブルを設定する方法について学んでみます。

まずは、以下のコードを見てみてください。

--テーブルの定義
local table1 = {6, 4, 9, 8}

-- メタテーブルの定義
local mt = {
	__add = function(t, val)
		local result = {}
		for i, v in ipairs(t) do
			result[i] = v + val
		end
		return result
	end
}

-- table1にメタテーブルを設定
setmetatable(table1, mt)

-- 計算
local result = table1 + 2 + 5

-- 印刷
print(result)

先ほどのコードとの違いは、計算部分に+5 が追加されたことです。

-- 計算
local result = table1 + 2 + 5

それでは、このコードをコピーして、実行してみましょう!

…どうなりましたか?

このように、エラーが発生したはずです。

このエラーは、テーブルにメタテーブルが設定されていなかった時と同じエラーですね。

しかし、テーブルにメタテーブルは設定されているはずです。変更点は、計算部分に+5 を追加しただけ。どうして、エラーが発生してしまったのでしょうか?

その答えは、新しいテーブルがメタテーブルではなくなってしまったためです。

どういうことなのか解説します。
まず、この関数部分では、新しいresult テーブルを作成し、テーブルと値をもとに、そのテーブルを返しています。

		local result = {}
		for i, v in ipairs(t) do
			result[i] = v + val
		end
		return result

よく考えてみてください。result テーブルはあくまで、元のテーブルのデータをもとに作成されたものであって、元のテーブルそのものではないのです。

関数内で新しくテーブルを作成しているので、そのテーブルにメタテーブルは設定されていないというわけなんですね。

そのため、メタテーブル + 2 + 5普通のテーブル + 5 となり、エラーが発生してしまったのです。

ということで、エラーを解消するには、新しいテーブルにもsetmetatable() を実行する必要があります。

setmetatable() の第一引数には、result テーブルを指定すればよいですが、第二引数のメタテーブルはどう指定すればいいでしょうか?

元のテーブルに設定されているメタテーブルと同じものを設定したいので、そのテーブルのメタテーブルを取得する関数を使います。

それが、getmetatable() です。setがgetに変わっただけですね。

第一引数にメタテーブルを取得したいテーブルをいれれば、返り値としてメタテーブルが返えってきます。

これを踏まえて、コードに以下の関数を追加すれば、OKです。

		setmetatable(result, getmetatable(t))

これで、関数全体はこのようになります。

function(t, val)
    local result = {}
    for i, v in ipairs(t) do
        result[i] = v + val
    end
    setmetatable(result, getmetatable(t))
    return result
end

コードを修正できたら、もう一度実行してみましょう!

エラーが発生せずに、想定どおりの結果をだすことができました!

getmetatable() も、よく使う関数なので覚えておきましょう!

その他の演算子関連のメタメソッドや補足事項

このセクションでは、その他、メタテーブルを扱う上で必要な基本的な情報を補足したいと思います。

最初に、算術演算子に関連するメタメソッドの一覧をご紹介します。

メタメソッド対応する算術演算子説明
__add+加算 (例: a + b)
__sub-減算 (例: a - b)
__mul*乗算 (例: a * b)
__div/除算 (例: a / b)
__mod%剰余 (例: a % b)
__pow^累乗 (例: a ^ b)
__unm- (単項)負数化 (例: -a)

算術演算子に関連するメタメソッドとしては、上記7つがあります!これら全てを暗記する必要はないと思います。スクショかなにかに保存して、いつでも見返せるようにしておけばOK。使っていくうちに自然と覚えられればいいですね!

次に、比較演算子のメタメソッドを紹介します。

メタメソッド対応する算術演算子説明
__eq==等しいかどうか (例: a == b)
__It<小さいかどうか (例: a < b)
__le<=小さいor等しいかどうか (例: a <= b)

比較演算子のコードの例は次のとおりです。(ChatGPTに書いてもらったものを少し手直ししました。象とねずみの例で分かりやすいですね)

-- 動物の重さを管理するテーブル
local elephant = {weight = 5000}  -- 象の重さは5000kg
local mouse = {weight = 0.05}     -- ねずみの重さは0.05kg

-- メタテーブルを作成して、比較演算のルールを定義
local meta = {}

-- 等しいかどうかを比較するメタメソッド
meta.__eq = function(animal1, animal2)
    return animal1.weight == animal2.weight
end

-- 小さいかどうかを比較するメタメソッド
meta.__lt = function(animal1, animal2)
    return animal1.weight < animal2.weight
end

-- 小さいかまたは等しいかどうかを比較するメタメソッド
meta.__le = function(animal1, animal2)
    return animal1.weight <= animal2.weight
end

-- 各動物のテーブルにメタテーブルを設定
setmetatable(elephant, meta)
setmetatable(mouse, meta)

-- 動物の重さを比較してみる
print(elephant == mouse)  -- 象とねずみの重さは同じですか?(falseと表示される)
print(elephant < mouse)   -- 象はねずみより軽いですか?(falseと表示される)
print(elephant > mouse)   -- <の結果を反転した結果 (trueと表示される)
print(elephant <= mouse)  -- 象はねずみより軽いか、または同じ重さですか?(falseと表示)
print(mouse <= elephant)  -- ねずみは象より軽いか、または同じ重さですか?(trueと表示される)
比較演算子について補足

Luaでは>(大きい)や>=(大きいか等しい)の動作をメタテーブルによってカスタマイズしなくても、__lt__leが定義されていると自動的に反転した結果を使います。

例えば、このコードの例の場合、elephant > mousetrueのとき、elephant < mousefalseとして動作します。

以上が比較演算子についての紹介でした。

また、メタテーブル全体において、2つ、補足事項があります。

メタテーブルについて補足
  • メタテーブルはテーブルで定義するので、柔軟。例えば、1つのメタテーブルに複数のメタメソッドを設定することができるし、スクリプトの途中で動的にメタメソッドやその関数を変えることもできる。
  • 1つのテーブルには1つのメタテーブルしか設定できない。(上書きされる)

以上で、メタテーブルの基礎的な部分は理解できたはずです。

まだ理解できない方は、何度でも読み返してくださいね。

特殊なメタメソッド

ここまでは、メタテーブルの基本的な概念と、算術演算子・比較演算子のメタメソッドを紹介しました。

ここからは、主要なその他の特殊な機能を持ったメタメソッドについて紹介したいと思います。

これらのメタメソッドを扱えるようになって、初めてメタテーブルの真価を発揮できます。

__index メタメソッド

__index は、最も使用頻度が高いとされているメタメソッドです。

このメタメソッドは、テーブルでアクセスしようとしているキーが存在しない場合に呼び出されます。

例えば以下のように、テーブルに存在しないキーにアクセスしようとすることで、"default-value" というデフォルト値を設定することが可能です。(このメタメソッドを設定していない場合、出力結果はnilになります。)

local myTable = {}
local mt = {
    __index = function(table, key)
        return "default-value"
    end
}
setmetatable(myTable, mt)

print(myTable.someKey)  -- 出力結果: "default-value"

テーブルにデフォルト値を設定できるのは、かなり便利なのではないでしょうか。

また、__index は、別のテーブルからデータを取得する用途としても利用できます。

local defaultValues = {x = 10, y = 20}
local myTable = {}
local mt = {
    __index = defaultValues  -- 別のテーブルを参照する
}
setmetatable(myTable, mt)

-- キー "x" や "y" は myTable には存在しないが、defaultValues から取得される
print(myTable.x)  -- 出力結果: 10
print(myTable.y)  -- 出力結果: 20

これを上手く活用すれば、コードの再利用性や柔軟性を高めることができますね。

以上が、全メタメソッドの中で最も使用頻度が多いであろうメタメソッド、__index の紹介でした。

__newindex メタメソッド

__newindex は、__index の次に使用頻度が多いとされているメタメソッドです。

__newindexを使えば、新しいキーを追加しようとしたときに、その動作をカスタマイズすることができます。

このメタメソッドの使用方法は少し特殊です。以下に例を示します。

local myTable = {}
local mt = {
    __newindex = function(table, key, value)
        print("Attempting to assign:", key, "=", value)
        rawset(table, key, value)  -- 通常のテーブルに書き込む動作を実行
    end
}
setmetatable(myTable, mt)

myTable.newKey = "Hello"  -- "Attempting to assign: newKey = Hello" と表示される

__newindex では、3つの引数が渡されます。それぞれ、以下のようになっています。

  • 書き込み対象のテーブル (table)
  • 書き込み対象のキー (key)
  • そのキーに割り当てる値 (value)

他のメタメソッドと異なる注意点として、return は使用せずに、rawset() を使用してテーブルの操作を行います。

rawset() は、__newindex を含むメタメソッドの影響を受けずに、指定したテーブルに指定したキーと値のペアを追加することができる関数です。

これにより、__newindex の無限ループを防ぐことができます。rawset() は他の場所でも使うことがあるので、覚えておきましょう。

__newindex を活用すれば、以下のようなことが可能になります。

  • テーブルに対してキーの追加を防げる
  • 値をチェックして、不正な値を持ったキーの追加を拒否できる
  • その他、値に対して演算を適用してからテーブルに挿入させたりすることが可能に

例えば、数値以外の型のデータをテーブルに挿入できなくすることが可能です。

local myTable = {}
local mt = {
    __newindex = function(table, key, value)
        if typeof(value) ~= "number" then
            error("このテーブルに挿入できるのは数値のみです。")
        end
        rawset(table, key, value) -- 実際のテーブルに値を設定
    end
}

setmetatable(myTable, mt)

myTable.a = 10 -- これは成功する
myTable.b = "string" -- これはエラーになる

__newindex は少し扱い方が特殊ですが、とても便利なメタメソッドの1つなので、自身のプロジェクトに合わせて上手く活用してみてください。

__call メタメソッド

__call メタメソッドを使えば、テーブルを関数のように振る舞わせることができます。

コードの例を以下に示します。

local myTable = {}
local mt = {
    __call = function(table, arg1, arg2)
        print("テーブルが呼び出されました。引数:", arg1, arg2)
    end
}
setmetatable(myTable, mt)

myTable(10, 20)  -- 出力結果: "テーブルが呼び出されました。引数: 10 20"

__tostring メタメソッド

__tostring メタメソッドは、テーブルが文字列に変換されるときに呼び出されます。

つまりは、テーブルに対して、tostring()print() などを適用したときの動作をカスタマイズできます。テーブルの表示方法をカスタマイズすることができます。

以下にコード例を示します。

local person = {
    name = "Alice",
    age = 30
}

local mt = {
    __tostring = function(table)
        return "Name: " .. table.name .. ", Age: " .. table.age
    end
}
setmetatable(person, mt)

print(person)  -- 出力結果: "Name: Alice, Age: 30"

注意点として、__tostring メタメソッドでは、必ず文字列を返すようにしてください。もし他のデータ型を返すと、エラーが発生します。

__tostring を使えば、このようにしてテーブルの出力などをカスタマイズすることができます。

これで、よく使われる特殊なメタメソッドの紹介は以上です。上手く使いこなしてメタテーブルをマスターしましょう!

最後に

この記事では、メタテーブルの扱い方について紹介しました。

この記事の内容を理解すれば、テーブルをより柔軟に活用することができるはずです。

次の記事では、このメタテーブルを使って、独自のインスタンスをつくることができるOOP手法について紹介したいと思います。少し難しいですが、それもきっと役に立つはずです。ぜひお楽しみに!

それでは、今回はここまで。最後までご覧いただきありがとうございました。