Lua入門:tableの全てを徹底解説【初心者向け】
Luaというプログラミング言語をご存知でしょうか?軽量で高速、他のアプリケーションに組み込みやすい(スクリプト言語として利用されることが多い)という特徴を持ち、ゲーム開発(特にRobloxや、かつての携帯ゲーム機の一部)、設定ファイル、組み込みシステムなど、様々な分野で活躍しています。
Luaを学ぶ上で、最も、そしておそらく唯一と言っていいほど重要なデータ構造があります。それが「table(テーブル)」です。C言語における配列や構造体、Pythonにおけるリストや辞書、JavaScriptにおける配列やオブジェクトといった、他の言語であれば複数のデータ型で表現されるものが、Luaではほぼ全てこの「table」一つでまかなわれます。
tableをマスターすることは、Luaをマスターすることと言っても過言ではありません。しかし、「一つのデータ型で何でもできる」というのは、逆に言えばその内部的な仕組みや多様な使い方を理解しないと、戸惑うことも多いでしょう。
この記事では、Luaのtableについて、プログラミング初心者の方でも理解できるよう、基礎の基礎から応用的な使い方まで、約5000語のボリュームで徹底的に解説します。この記事を読めば、Luaのtableの「なぜ?」や「どうやって?」がクリアになり、自信を持ってLuaプログラミングに進めるようになるはずです。
さあ、Luaの魔法の箱、tableの世界へ飛び込みましょう!
1. はじめに:LuaとTableの重要性
まず、Luaがどんな言語か、そしてなぜtableがそれほど重要なのかを簡単に説明します。
Luaは、シンプルで軽量なスクリプト言語として設計されました。C言語で記述されており、非常に高速に動作します。その大きな特徴の一つは、非常に小さなコア部分に強力な機能を詰め込んでいることです。そして、その「強力な機能」の大部分を担っているのが、これから学ぶ「table」なのです。
他の多くの言語では、データの集合を扱うために「配列」「リスト」「辞書(ハッシュマップ、連想配列)」「構造体」「オブジェクト」など、様々なデータ型が用意されています。それぞれに得意なこと、苦手なことがあります。
しかしLuaでは、これらの概念の多くを、たった一つのデータ型であるtableで表現します。
- 順番に並んだデータの集まり(配列)
- 名前と値のペアの集まり(辞書、連想配列)
- 複数の関連するデータをまとめたもの(構造体、オブジェクト)
これら全てが、Luaではtableなのです。
なぜこのような設計になっているのでしょうか?それは、言語のシンプルさを保ちつつ、最大限の柔軟性を持たせるためです。tableは非常に柔軟性が高く、様々なデータの表現に順応できます。
そのため、Luaプログラミングの学習は、「tableの使い方を深く理解すること」と言い換えることができます。この記事で、tableの基本から応用までをじっくりと学び、Luaの強力さを実感してください。
2. Tableの超基本:定義と中身
tableの最も基本的なことから始めましょう。tableとは何で、どうやって作るのでしょうか?
2.1 Tableとは?
Tableは、キー(key)と値(value)のペアの集まりです。それぞれの「キー」に対応する「値」が格納されています。ちょうど、辞書で「単語(キー)」に対応する「意味(値)」が載っているようなイメージです。
この「キー」には、ほとんど全ての種類のデータを使うことができます(ただし、nilとNaNはキーとして使えません。理由は後述します)。そして、「値」には、これまたLuaのあらゆる種類のデータを格納できます。数値、文字列、真偽値、関数、さらには別のtable自身も値として格納できます。
tableはオブジェクトです。そして、Luaでは参照型のデータです。これはどういうことかというと、tableを変数に代入したり、関数の引数として渡したりする場合、tableそのもののコピーが作られるのではなく、「そのtableがメモリ上のどこにあるか」という情報(参照)が渡されるということです。この「参照」という概念は、tableを扱う上で非常に重要なので、後ほど詳しく解説します。
2.2 Tableの作成方法
tableを作るのは非常に簡単です。波括弧 {} を使うだけです。
“`lua
— 空のtableを作成
local my_table = {}
— tableの中身を表示してみる(初期状態なので空)
print(type(my_table)) — 出力: table
print(my_table) — 出力: table: <メモリ上のアドレス> のようなもの
“`
これだけで、メモリ上に新しい空のtableが作成され、変数 my_table にそのtableへの「参照」が格納されます。
作成時に、あらかじめキーと値のペアを指定して初期化することもできます。初期化の方法にはいくつか種類があります。
方法1:連想配列(キーと値のペア)形式
キーと値を = で結び、ペアをカンマ , またはセミコロン ; で区切ります。
“`lua
— 文字列をキーとしたtable
local person = {
name = “Alice”,
age = 30,
city = “Tokyo”
}
print(person) — tableへの参照が出力される
“`
この例では、キーは "name", "age", "city" という文字列で、対応する値は "Alice", 30, "Tokyo" です。
キーを [ ] で囲むと、文字列以外の型のキーも使えます。
“`lua
— 数値や真偽値をキーとしたtable
local data = {
[1] = “first item”,
[2] = “second item”,
[true] = “yes”,
[“hello”] = “world”,
[my_table] = “this table is a key!” — なんと、別のtableもキーにできる!
}
print(data)
“`
キーを [ ] で囲む記法は、文字列キーの場合でも使えます。上の person の例は、実は name = "Alice" の部分が ["name"] = "Alice" の省略形なのです。つまり、以下の2つは全く同じ意味です。
“`lua
local person1 = {
name = “Alice”,
age = 30
}
local person2 = {
[“name”] = “Alice”,
[“age”] = 30
}
“`
方法2:配列形式
キーを指定せず、値だけをカンマ , またはセミコロン ; で区切って並べると、特別なtableが作成されます。Luaでは、このようなtableを配列として扱うことがよくあります。
“`lua
— 値だけを並べたtable
local fruits = {“apple”, “banana”, “cherry”}
print(fruits)
“`
この形式で初期化されたtableでは、Luaが自動的に連番の整数キーを1から割り当てます。つまり、上の fruits は、内部的には以下のように初期化されたtableと同じになります。
lua
local fruits_internal = {
[1] = "apple",
[2] = "banana",
[3] = "cherry"
}
重要ポイント: Luaの配列は、他の多くの言語と異なり、1から始まります。これはLuaの大きな特徴の一つです。
方法3:混在形式
もちろん、これらの形式を混ぜることも可能です。
“`lua
local complex_data = {
“これは配列部分”, — 自動的にキー1が割り当てられる
“これも配列部分”, — 自動的にキー2が割り当てられる
name = “Alice”, — 文字列キー
age = 30, — 文字列キー
[100] = “特別な番号”, — 明示的に指定した数値キー
[true] = false — 真偽値キー
}
print(complex_data)
“`
このように、一つのtableの中に、自動的に割り当てられた連番の整数キーを持つ部分と、明示的に指定した様々な型のキーを持つ部分が混在できます。これが、Luaのtableが柔軟であるゆえんです。
3. Tableへのアクセス:中身の取り出しと変更
Tableに格納した値は、対応するキーを使ってアクセスできます。アクセスする方法も、初期化の方法と同様にいくつか種類があります。
3.1 値の取得
tableから値を取り出すには、tableの変数名の後にキーを指定します。キーの指定方法には「ドット記法」と「角括弧記法」があります。
ドット記法 (.)
キーが有効なLuaの識別子である文字列の場合に使えます。有効な識別子とは、アルファベット(大文字・小文字)、数字、アンダースコア _ で構成され、数字で始まらない文字列です(予約語は使えません)。
“`lua
local person = {
name = “Alice”,
age = 30,
city = “Tokyo”
}
print(person.name) — 出力: Alice
print(person.age) — 出力: 30
print(person.city) — 出力: Tokyo
“`
これは、他の言語のオブジェクトのプロパティや構造体のメンバにアクセスする記法に似ていますね。非常に直感的でよく使われます。
角括弧記法 ([])
キーが文字列以外の型の場合や、キーが変数に格納されている場合、あるいはキーにスペースやハイフンなど、ドット記法で使えない文字が含まれる文字列の場合に使います。キーを [ ] で囲みます。
“`lua
local data = {
[1] = “first”,
[2] = “second”,
[“item-name”] = “special item”,
[true] = “boolean key”,
[my_table] = “table as key value” — my_tableは前述の空テーブル
}
print(data[1]) — 出力: first (数値キー)
print(data[2]) — 出力: second (数値キー)
print(data[“item-name”]) — 出力: special item (ハイフンを含む文字列キー)
print(data[true]) — 出力: boolean key (真偽値キー)
print(data[my_table]) — 出力: table as key value (tableをキーとしてアクセス)
“`
キーが文字列の場合、ドット記法と角括弧記法は置き換え可能です。
“`lua
local person = {
name = “Alice”,
age = 30
}
print(person.name) — ドット記法
print(person[“name”]) — 角括弧記法 (文字列リテラルをキーとして指定)
local key_name = “age”
print(person[key_name]) — 角括弧記法 (変数をキーとして指定)
— print(person.key_name) — これはエラーまたはnilになります!
— ドット記法はドットの後に書かれたものを直接キー名とみなすため、
— “key_name”という名前のキーを探しに行ってしまいます。
“`
このように、キーが変数に入っている場合は、必ず角括弧記法 [] を使います。これが、ドット記法と角括弧記法を使い分ける最も一般的な理由です。
3.2 存在しないキーへのアクセス
もし、tableに存在しないキーを使って値にアクセスしようとしたらどうなるでしょうか?エラーにはなりません。Luaでは、存在しないキーにアクセスすると、特別な値である nil が返されます。
“`lua
local person = {
name = “Alice”,
age = 30
}
print(person.name) — 出力: Alice
print(person.city) — 出力: nil (cityというキーは存在しない)
print(person[1]) — 出力: nil (1というキーは存在しない)
“`
この挙動は、tableのキーの存在を確認する際によく利用されます。
3.3 値の変更と追加
キーを使ってアクセスできるのは、値の取得だけではありません。値を代入することで、既存のキーの値の変更や、新しいキーと値のペアの追加ができます。
“`lua
local person = {
name = “Alice”,
age = 30
}
— 既存のキーの値を変更
person.age = 31
print(person.age) — 出力: 31
— 新しいキーと値のペアを追加
person.city = “Osaka”
print(person.city) — 出力: Osaka
— 角括弧記法でも同様
person[“job”] = “Engineer”
print(person[“job”]) — 出力: Engineer
local new_key = “salary”
person[new_key] = 6000 — 変数をキーにして追加
print(person[new_key]) — 出力: 6000
— 配列のようなtableにも追加
local fruits = {“apple”, “banana”}
fruits[3] = “cherry” — キー3を追加(配列の末尾に追加したような形に)
fruits[1] = “red apple” — 既存のキー1の値を変更
print(fruits[1]) — 出力: red apple
print(fruits[3]) — 出力: cherry
print(fruits[2]) — 出力: banana
“`
このように、ドット記法や角括弧記法を使って値を代入するだけで、tableの中身を自由に変更・追加できます。
3.4 キーの削除
tableからキーと値のペアを削除したい場合は、そのキーに対応する値に nil を代入します。
“`lua
local person = {
name = “Alice”,
age = 30,
city = “Tokyo”
}
print(person.city) — 出力: Tokyo
— cityキーを削除
person.city = nil
print(person.city) — 出力: nil (削除されたので存在しない扱いになる)
— 削除されたキーにアクセスしてもnilが返ることは、存在しないキーと同じ挙動
— ただし、メモリ上ではそのキーと値のペアが解放される可能性がある(ガベージコレクションに任される)
“`
nil を値として代入することは、そのキーと値のペアをtableから取り除くことを意味します。これは、Luaでtableの要素を「削除」する標準的な方法です。
4. Tableの種類:配列と連想配列(辞書)としての側面
前述の通り、Luaのtableは配列としても、連想配列(辞書)としても使えます。これは、tableが内部的に配列部分とハッシュ部分という二つの異なる構造を組み合わせてデータを管理しているためです。
4.1 配列部分とハッシュ部分
- 配列部分: キーが1から始まる連続した正の整数であるペアが効率的に格納されます。メモリ上で連続した領域に配置されることが多いです。
- ハッシュ部分: それ以外のキー(文字列、真偽値、nil以外の数値、tableなど)を持つペアが格納されます。ハッシュテーブルと呼ばれる構造を使って、キーから値への高速なアクセスを可能にしています。
Luaのtableは、この二つを一つのデータ型として提供しています。ユーザーは特に意識することなく、数値キーも文字列キーも同じtableに対して利用できます。
例えば、以下のtableを作成したとします。
lua
local my_data = {
"apple", -- キー1 (配列部分)
"banana", -- キー2 (配列部分)
name = "Alice", -- キー"name" (ハッシュ部分)
[10] = "extra", -- キー10 (ハッシュ部分 - 連続していない数値キー)
[true] = false -- キーtrue (ハッシュ部分)
}
この my_data という一つのtableの中に、キー1と2を持つ「配列っぽい」部分と、キー"name"、10、trueを持つ「辞書っぽい」部分が共存しています。
4.2 # 演算子と配列の長さ
tableの「長さ」を取得するために、Luaには単項演算子 # が用意されています。
lua
local list = {"a", "b", "c", "d"}
print(#list) -- 出力: 4
# 演算子は、tableの配列部分の長さを返します。具体的には、「1から始まる連番の整数キーが途切れるまでの最後のキー」を返します。
“`lua
local data = {
“first”, — キー1
“second”, — キー2
[10] = “tenth”, — キー10
[“name”] = “Lua”
}
print(#data) — 出力: 2 (キー1, 2は連続しているが、3以降がないため2が返る)
local sparse = {
[1] = “one”,
[3] = “three”,
[5] = “five”
}
print(#sparse) — 出力: 1 (キー1の次は3で途切れるため、1が返る)
“`
もしtableの配列部分が空の場合、#table は 0 を返します。
“`lua
local empty_list = {}
print(#empty_list) — 出力: 0
local dict = { name = “Lua”, version = 5.4 }
print(#dict) — 出力: 0 (配列部分がないため)
“`
# 演算子は、キーが連続した正の整数であることを前提として配列の長さを計算します。配列部分の途中に nil があったり、キーが歯抜けになっていたりすると、期待通りの長さを返さない可能性があることに注意が必要です。しかし、一般的な「1から始まる連続した整数のキーを持つ配列」に対しては、正確な長さを返してくれます。
5. Table操作のための標準ライブラリ
Luaの標準ライブラリには、tableを操作するための便利な関数がいくつか用意されています。特に配列としてtableを扱う際によく使われます。これらの関数は table という名前のtableの中にまとめられています。
5.1 table.insert(table, [pos,] value)
この関数は、tableの指定した位置 pos に value を挿入します。pos を省略した場合は、配列の末尾(現在の長さの次のインデックス)に挿入されます。挿入により、指定位置以降の要素のインデックスは一つずつずれます。
“`lua
local fruits = {“apple”, “banana”, “cherry”} — 長さ3
— 末尾に挿入 (posを省略)
table.insert(fruits, “grape”)
— fruitsは {“apple”, “banana”, “cherry”, “grape”} になる
print(#fruits) — 出力: 4
print(fruits[4]) — 出力: grape
— 2番目の位置に挿入
table.insert(fruits, 2, “orange”)
— fruitsは {“apple”, “orange”, “banana”, “cherry”, “grape”} になる
print(#fruits) — 出力: 5
print(fruits[2]) — 出力: orange
print(fruits[3]) — 出力: banana (元々2番目だった要素がずれた)
“`
table.insert は主に配列として扱っているtableに対して使用します。ハッシュ部分には影響しません。
5.2 table.remove(table, [pos])
この関数は、tableの指定した位置 pos の要素を削除します。削除された要素のインデックスより大きいインデックスを持つ要素は、インデックスが一つずつ前にずれます。pos を省略した場合は、配列の末尾の要素が削除されます。削除された値が戻り値として返されます。
“`lua
local fruits = {“apple”, “orange”, “banana”, “cherry”, “grape”} — 長さ5
— 3番目の要素を削除
local removed_fruit = table.remove(fruits, 3) — “banana”が削除され、返される
— fruitsは {“apple”, “orange”, “cherry”, “grape”} になる
print(removed_fruit) — 出力: banana
print(#fruits) — 出力: 4
print(fruits[3]) — 出力: cherry (元々4番目だった要素がずれた)
— 末尾の要素を削除 (posを省略)
local last_fruit = table.remove(fruits) — “grape”が削除され、返される
— fruitsは {“apple”, “orange”, “cherry”} になる
print(last_fruit) — 出力: grape
print(#fruits) — 出力: 3
“`
table.remove も table.insert と同様に、主に配列として扱っているtableに対して使用します。配列部分の途中の要素を削除すると、それ以降の要素のインデックスが全てずれるため、ループ処理中に使用する際は注意が必要です(後ろから削除するか、ループのインデックス調整が必要)。
5.3 table.concat(list, sep, start, end)
この関数は、tableの配列部分の要素を文字列として結合します。
* list: 結合するtable(配列であると仮定される)
* sep: 要素間に挿入する区切り文字列(省略可能、デフォルトは空文字列)
* start: 結合を開始するインデックス(省略可能、デフォルトは1)
* end: 結合を終了するインデックス(省略可能、デフォルトは #list)
“`lua
local words = {“Hello”, ” “, “World”, “!”}
local sentence = table.concat(words) — デフォルトの区切り文字と範囲で結合
print(sentence) — 出力: Hello World!
local fruits = {“apple”, “banana”, “cherry”}
local fruit_list = table.concat(fruits, “, “) — “, “を区切り文字として結合
print(fruit_list) — 出力: apple, banana, cherry
local numbers = {10, 20, 30, 40, 50}
local sub_list = table.concat(numbers, “-“, 2, 4) — 2番目から4番目までを”-“で結合
print(sub_list) — 出力: 20-30-40
“`
table.concat は、要素を文字列に変換してから結合します。数値などは自動的に文字列に変換されます。
5.4 table.sort(list, comp)
この関数は、tableの配列部分の要素をソートします。ソートは破壊的に行われます(元のtableの並び順が変わります)。
* list: ソートするtable(配列であると仮定される)
* comp: 比較関数(省略可能)。省略した場合、標準的な順序(数値なら昇順、文字列なら辞書順など)でソートされます。比較関数を指定する場合は、二つの引数を取り、第一引数が第二引数より「前」に来るべきなら true を返す関数を指定します(例えば、降順ソートなら function(a, b) return a > b end)。
“`lua
local numbers = {5, 2, 8, 1, 9, 4}
table.sort(numbers) — 標準的な昇順ソート
— numbersは {1, 2, 4, 5, 8, 9} になる
for i = 1, #numbers do
print(numbers[i])
end
local names = {“Charlie”, “Alice”, “Bob”}
table.sort(names) — 標準的な辞書順ソート
— namesは {“Alice”, “Bob”, “Charlie”} になる
for i = 1, #names do
print(names[i])
end
— 降順にソートする場合
local scores = {85, 92, 78, 95}
table.sort(scores, function(a, b) return a > b end) — aがbより大きいならtrue(降順)
— scoresは {95, 92, 85, 78} になる
for i = 1, #scores do
print(scores[i])
end
“`
table.sort も # 演算子や table.insert/table.remove と同様に、tableの配列部分に対して動作します。ソートの対象となるのは、1から #list までのインデックスを持つ要素です。
6. Tableのイテレーション(繰り返し処理)
tableの全てのキーと値のペア、または配列部分の要素を順番に取り出して処理したい場合があります。このような繰り返し処理を「イテレーション」または「走査」と呼びます。Luaでは、for ループと特定のイテレータ関数を使ってtableをイテレーションします。
6.1 数値forループ(配列の場合)
tableを配列として扱っている(キーが1から始まる連続した整数である)場合は、数値forループと # 演算子を使ってイテレーションするのが最も一般的です。
“`lua
local fruits = {“apple”, “banana”, “cherry”}
— インデックス i を 1 から #fruits (この場合3) まで変化させる
for i = 1, #fruits do
print(“Index:”, i, “Value:”, fruits[i])
end
— 出力:
— Index: 1 Value: apple
— Index: 2 Value: banana
— Index: 3 Value: cherry
“`
これは、インデックスを使って配列の要素にアクセスする、他の多くの言語のforループと似ています。# 演算子が配列部分の長さを正確に返してくれるため、このようにシンプルに記述できます。
ただし、前述の通り、# 演算子はキーが連続していない場合には期待通りの長さを返さない可能性があるため、数値forループはキーが1から始まる連続した整数であることが分かっている場合にのみ安全に使用できます。
6.2 汎用forループとイテレータ
Luaには、数値forループよりも柔軟な汎用forループがあります。これは、イテレータ関数と組み合わせて使用し、tableのキーと値のペアを順番に取り出すことができます。
汎用forループの構文は以下の通りです。
lua
for key1, key2, ... in iterator_function, state, initial_value do
-- ループ本体
end
tableのイテレーションでは、通常、イテレータ関数として pairs または ipairs を使い、state と initial_value としてイテレーションしたいtableを指定します。
lua
-- イテレータ関数としてpairsまたはipairs、状態としてtableを指定
for key, value in iterator_function(table) do
-- key と value を使った処理
end
pairs と ipairs は、それぞれtableのイテレーションにおいて異なる役割を持ちます。
6.3 pairs(table) イテレータ
pairs は、tableの全てのキーと値のペアを走査します。配列部分とハッシュ部分の両方を対象とします。
“`lua
local data = {
“first”, — キー1 (配列部分)
“second”, — キー2 (配列部分)
name = “Alice”, — キー”name” (ハッシュ部分)
[10] = “tenth”, — キー10 (ハッシュ部分)
[true] = false — キーtrue (ハッシュ部分)
}
print(“Using pairs:”)
for key, value in pairs(data) do
print(“Key:”, key, “Value:”, value)
end
— 出力例(順序は保証されない!):
— Using pairs:
— Key: 1 Value: first
— Key: 2 Value: second
— Key: name Value: Alice
— Key: 10 Value: tenth
— Key: true Value: false
“`
重要な注意点として、pairs で走査する際のキーと値のペアが取り出される順序は保証されません。これは、tableのハッシュ部分の内部構造に依存するためです。特定の順序で処理したい場合は、キーを別途リストにしてソートするなど、別の方法を取る必要があります。
pairs は、tableを「キーと値のペアの集まり」として一般的に扱いたい場合に適しています。
6.4 ipairs(table) イテレータ
ipairs は、tableの配列部分、つまり1から始まる連番の整数キーを持つ要素のみを走査します。連続する整数キーが見つからなくなった時点で走査を終了します。
“`lua
local data = {
“first”, — キー1
“second”, — キー2
[10] = “tenth”, — キー10 (スキップされる)
[“name”] = “Alice” — キー”name” (スキップされる)
}
print(“Using ipairs:”)
for index, value in ipairs(data) do
print(“Index:”, index, “Value:”, value)
end
— 出力:
— Using ipairs:
— Index: 1 Value: first
— Index: 2 Value: second
“`
この例では、キー1と2は連続していますが、次に続くはずのキー3が存在しないため、ipairs はキー2まで走査して終了します。キー10やキー"name" は対象外となります。
“`lua
local sparse_list = {
[1] = “one”,
[3] = “three”, — キー2がないのでここで終了
[5] = “five”
}
print(“Using ipairs on sparse_list:”)
for index, value in ipairs(sparse_list) do
print(“Index:”, index, “Value:”, value)
end
— 出力:
— Using ipairs on sparse_list:
— Index: 1 Value: one
“`
ipairs は、tableを配列として扱いたい場合に適しています。順序が保証され、かつ高速に配列部分を走査できます。ただし、キーが連続していない「スパースな配列」を完全に走査したい場合には適していません。
6.5 イテレーション中のtable変更に関する注意
pairs や ipairs を使ってtableを走査している最中に、そのtableの中身(キーや値)を変更したり削除したりすると、予期しない挙動を引き起こす可能性があります。特に pairs は順序が不定であることもあり、危険です。
もしイテレーション中にtableを変更する必要がある場合は、事前に処理対象のキーや値のリストを作成しておき、そのリストを使って処理を実行するのが安全な方法です。
“`lua
local my_list = {“a”, “b”, “c”, “d”}
local items_to_process = {}
— 処理したい要素を一時的なリストにコピー
for i = 1, #my_list do
table.insert(items_to_process, my_list[i])
end
— コピーしたリストを使って処理
for i = 1, #items_to_process do
local item = items_to_process[i]
print(“Processing:”, item)
— ここで元のmy_listに対して安全に変更を加えることができる
— 例: my_list[i] = string.upper(item)
end
“`
7. Tableを使ったより高度な使い方
Tableはその柔軟性から、単なるデータの集まり以上のものを表現するために使われます。ここでは、ネストしたtableや、tableを使ったオブジェクト指向プログラミングの基本的な考え方、そしてメタテーブルのさわりについて解説します。
7.1 ネストしたTable
tableの値として別のtableを格納することで、より複雑な構造を持つデータを表現できます。これは「ネストしたtable」と呼ばれ、他の言語の多次元配列、構造体の中に別の構造体がある、JSONデータのような構造などを表現するのに使われます。
“`lua
— 人物のリスト(それぞれの人物がtableで表現されている)
local people = {
{ name = “Alice”, age = 30, city = “Tokyo” }, — 最初の人物(キー1)
{ name = “Bob”, age = 25, city = “Osaka” }, — 二番目の人物(キー2)
{ name = “Charlie”, age = 35, city = “Fukuoka” } — 三番目の人物(キー3)
}
— ネストしたtableへのアクセス
print(people[1].name) — 出力: Alice (peopleの1番目の要素(table)のnameキーの値)
print(people[2].age) — 出力: 25
print(people[3].city) — 出力: Fukuoka
— 人物のリストをイテレーション
for i = 1, #people do
local person = people[i] — 各要素(人物のtable)を取り出す
print(person.name .. ” is ” .. person.age .. ” years old.”)
end
— 出力:
— Alice is 30 years old.
— Bob is 25 years old.
— Charlie is 35 years old.
— より複雑なネスト構造
local company = {
name = “Tech Corp”,
departments = {
{
name = “Engineering”,
manager = “David”,
employees = {“Alice”, “Bob”}
},
{
name = “Sales”,
manager = “Eve”,
employees = {“Charlie”}
}
}
}
— ネストしたデータへのアクセス
print(company.name) — 出力: Tech Corp
print(company.departments[1].name) — 出力: Engineering
print(company.departments[1].manager) — 出力: David
print(company.departments[1].employees[1]) — 出力: Alice
print(company.departments[2].employees[1]) — 出力: Charlie
“`
ネストしたtableを使うことで、現実世界の複雑な関係性を持つデータをLua上で自然に表現できます。
7.2 Tableを使ったオブジェクト指向プログラミング(基本)
Luaは、tableとメタテーブルの仕組みを利用して、柔軟なオブジェクト指向プログラミング(OOP)をサポートします。他の言語のようなクラス構文はありませんが、tableを使ってオブジェクトとそのメソッドを表現できます。
基本的な考え方は、tableを「オブジェクト」とし、そのtableの中に格納された「関数」を「メソッド」として扱うというものです。
“`lua
— Personという「クラス」の役割を担うtableを作成
local Person = {}
Person.__index = Person — 後述するメタテーブルの仕組みで必要
— コンストラクタ関数 (新しいPersonオブジェクトを作成し初期化する)
function Person:new(name, age)
local obj = { name = name, age = age } — 新しいオブジェクトとなるtableを作成
setmetatable(obj, self) — このobjにメタテーブルを設定する (selfは呼び出し元のPerson table)
return obj
end
— メソッドの定義
— Person.say_hello = function(self) とほぼ同じ意味
function Person:say_hello()
print(“Hello, my name is ” .. self.name)
end
function Person:celebrate_birthday()
self.age = self.age + 1
print(“Happy Birthday! I am now ” .. self.age .. ” years old.”)
end
— オブジェクトの作成
local alice = Person:new(“Alice”, 30)
local bob = Person:new(“Bob”, 25)
— メソッドの呼び出し
alice:say_hello() — 出力: Hello, my name is Alice
bob:celebrate_birthday() — 出力: Happy Birthday! I am now 26 years old.
bob:say_hello() — 出力: Hello, my name is Bob
“`
この例で注目すべきは、メソッド定義や呼び出しに使われているコロン : です。
- メソッド定義:
function Person:method_name(...)
これはPerson.method_name = function(self, ...)の糖衣構文(シンタックスシュガー)です。つまり、コロンを使ってメソッドを定義すると、その関数の第一引数として自動的にselfという名前の引数が追加されます。慣習的に、このselfにはメソッドが呼び出された「オブジェクト自身」が渡されます。 - メソッド呼び出し:
object:method_name(...)
これはobject.method_name(object, ...)の糖衣構文です。つまり、コロンを使ってメソッドを呼び出すと、ドット記法でアクセスした関数に対し、呼び出し元であるobject自身が第一引数として自動的に渡されます。
この : 記法と、self を使ってオブジェクト自身のプロパティ(self.name, self.ageなど)や他のメソッド(self:another_method())にアクセスすることで、オブジェクト指向らしいコードを書くことができます。
このOOPの仕組みの根幹には、次に説明する「メタテーブル」があります。
7.3 メタテーブルのさわり
メタテーブルは、Luaのtableの強力な機能の一つであり、tableの基本的な振る舞いをカスタマイズすることができます。メタテーブルは、あるtable(対象テーブル)に関連付けられた別のtableです。このメタテーブルに特定の「メタメソッド」と呼ばれる関数を定義することで、対象テーブルに対する特定の操作(例えば、存在しないキーへのアクセス、加算、文字列化など)が行われた際の挙動を変更できます。
ここでは、OOPと関連の深い __index というメタメソッドに絞って簡単に解説します。
__index メタメソッドは、tableに対して存在しないキーでアクセスがあった場合に呼ばれます。
対象テーブル t に対して t[key] というアクセスがあり、もし t の中に key というキーが存在しなかったとします。このとき、Luaはまず t にメタテーブルが設定されているかを確認します。
- もしメタテーブルが設定されていて、かつそのメタテーブルに
__indexというキーが存在する場合、Luaはその__indexの値を見に行きます。 __indexの値が関数であれば、その関数が(t, key)という引数で呼び出されます。その関数の戻り値が、元のt[key]アクセスの結果となります。__indexの値がtableであれば、Luaは代わりにその__indexに設定されたtableに対して、同じkeyでアクセスを試みます。そして、そのアクセス結果が元のt[key]アクセスの結果となります。
この「__index に設定されたtableに対してアクセスを試みる」という仕組みが、オブジェクト指向における「継承」のような振る舞いを実現します。
前述のPersonクラスの例を再度見てみましょう。
“`lua
local Person = {}
Person.__index = Person — Person table自身をメタテーブルの__indexに設定
function Person:new(name, age)
local obj = { name = name, age = age }
setmetatable(obj, self) — objのメタテーブルにPerson tableを設定 (selfはPerson)
return obj
end
function Person:say_hello()
print(“Hello, my name is ” .. self.name)
end
local alice = Person:new(“Alice”, 30)
— alice:say_hello() が呼ばれたとき…
— これは alice.say_hello(alice) の糖衣構文
— まず alice tableの中に “say_hello” というキーがあるか探す -> ない
— 次に alice のメタテーブル (Person table) を見る
— メタテーブルに __index キーがあるか探す -> ある (値は Person table自身)
— __index の値が table なので、その table (Person table) に対して “say_hello” というキーでアクセスを試みる
— Person tableの中に “say_hello” というキーがあるか探す -> ある (値は say_hello 関数)
— その関数が呼び出される。第一引数には元のオブジェクトである alice が渡される。
— 関数内で self.name は alice.name にアクセスし、”Alice” を出力する
“`
このように、オブジェクト alice 自身は say_hello 関数を持っていませんが、メタテーブルと __index の仕組みを通じて、元になった Person tableが持つ say_hello 関数を実行できるのです。これが、Luaでメソッドやプロパティの継承(またはデリゲーション)を実現する基本的な方法です。
メタテーブルには __index 以外にも様々なメタメソッドがあり、算術演算子(__add, __subなど)や比較演算子(__eq, __ltなど)、文字列化(__tostring)といった基本的な操作の挙動をカスタマイズできます。これらの機能を使いこなすことで、Luaのtableは非常に強力で柔軟なデータ型となりますが、初心者の方はまず __index による「継承のようなもの」の理解から始めるのが良いでしょう。
8. Tableのコピーと参照
Tableを扱う上で、他の基本的なデータ型(数値、文字列、真偽値)とは異なる重要な性質があります。それは、tableが参照型であるということです。
8.1 参照型であること
他の基本的なデータ型(プリミティブ型、値型と呼ばれることも)は、変数に代入されたり関数の引数として渡されたりする際に、その値そのものがコピーされます。
“`lua
local num1 = 10
local num2 = num1 — num1の値(10)がnum2にコピーされる
num2 = 20 — num2を変更してもnum1は変わらない
print(num1, num2) — 出力: 10 20
local str1 = “hello”
local str2 = str1 — str1の値(“hello”)がstr2にコピーされる
str2 = “world” — str2を変更してもstr1は変わらない
print(str1, str2) — 出力: hello world
“`
一方、tableは参照型です。変数にはtableそのものではなく、tableがメモリ上のどこにあるかを示す「参照(アドレスのようなもの)」が格納されます。tableを変数に代入すると、この参照がコピーされます。つまり、二つの変数が同じ一つのtableを参照することになります。
“`lua
local table1 = {1, 2, 3}
local table2 = table1 — table1が参照しているのと同じtableをtable2も参照するようになる
print(table1) — 出力例: table: 0x… (同じアドレス)
print(table2) — 出力例: table: 0x… (同じアドレス)
— table2を使ってtableの中身を変更する
table2[1] = 100
— table1で見ると、table2で行った変更が反映されている!
print(table1[1]) — 出力: 100
print(table2[1]) — 出力: 100
— table1を使ってtableに要素を追加する
table1[4] = 400
print(table1[4]) — 出力: 400
print(table2[4]) — 出力: 400 (これもtable2で見える)
— table1に新しいtableを代入しても、table2は元のtableを参照し続ける
table1 = {9, 8, 7}
print(table1[1]) — 出力: 9
print(table2[1]) — 出力: 100 (元のtableを参照し続けている)
“`
この「参照渡し」の性質を理解することは、tableを関数に渡したり、複数の場所で同じtableを共有したりする際に非常に重要です。意図しないtableの変更を防ぐため、あるいは意図的にtableを共有して状態を操作するために、この性質を意識する必要があります。
8.2 Tableのコピーが必要な場合
もし、元のtableとは独立した、全く新しいtableを作りたいが、中身は元のtableと同じにしておきたい(「コピー」したい)場合は、手動で新しいtableを作成し、元のtableの要素をコピーする必要があります。
コピーの方法には、「浅いコピー(Shallow Copy)」と「深いコピー(Deep Copy)」があります。
浅いコピー(Shallow Copy)
新しいtableを作成し、元のtableの第一階層のキーと値のペアをコピーします。値がtableや他の参照型データである場合、その参照がそのままコピーされるため、コピー先のtableとコピー元のtableは、ネストしたtableを共有することになります。
簡単な浅いコピーの例:
“`lua
local original = {
name = “Object A”,
data = {1, 2, 3} — ネストしたtable
}
— 浅いコピーを行う関数
local function shallow_copy(t)
local new_table = {}
for k, v in pairs(t) do
new_table[k] = v — 値をそのままコピー(参照もそのままコピーされる)
end
return new_table
end
local copied = shallow_copy(original)
print(copied.name) — 出力: Object A
print(copied.data[1]) — 出力: 1
— コピー先のnameを変更 -> originalには影響しない (文字列は値型)
copied.name = “Object B”
print(original.name) — 出力: Object A
print(copied.name) — 出力: Object B
— コピー先のネストしたtableの中身を変更
copied.data[1] = 99
— originalのネストしたtableの中身も変わっている!
print(original.data[1]) — 出力: 99 (参照が共有されているため)
print(copied.data[1]) — 出力: 99
“`
浅いコピーは、tableがネストしていない場合や、ネストしていても共有されても構わない場合にシンプルで高速です。
深いコピー(Deep Copy)
新しいtableを作成し、元のtableの全てのキーと値のペアを、ネストしたtableを含めて再帰的にコピーします。これにより、コピー先のtableは元のtableとは完全に独立したデータを持つことになります。
深いコピーは再帰が必要になるため、浅いコピーよりも少し複雑になります。循環参照(table Aがtable Bを参照し、table Bがtable Aを参照しているような状態)があると無限ループに陥る可能性があるため、既にコピー済みのtableを記録しておく必要があります。
簡単な深いコピーの例(循環参照対策なしのシンプルな例):
“`lua
— シンプルな深いコピーを行う関数(循環参照は考慮しない)
local function deep_copy_simple(t)
if type(t) ~= “table” then
return t — table以外はそのまま返す
end
local new_table = {}
for k, v in pairs(t) do
— キーも値も再帰的にコピーする
new_table[deep_copy_simple(k)] = deep_copy_simple(v)
end
return new_table
end
local original = {
name = “Object A”,
data = {1, 2, 3}
}
local copied = deep_copy_simple(original)
— コピー先のネストしたtableの中身を変更
copied.data[1] = 99
— originalのネストしたtableの中身は変わらない
print(original.data[1]) — 出力: 1
print(copied.data[1]) — 出力: 99
“`
より堅牢な深いコピー関数は、コピー元のtableとそのコピー先をマッピングする仕組み(別のtableを使う)を持つことで循環参照に対応します。実際のアプリケーションでは、このような頑丈な関数が必要になることが多いでしょう。
どちらのコピー方法を選ぶかは、tableの構造や、コピー後のtableと元のtableとの関係性をどのようにしたいかによって異なります。
9. Tableを使う上での注意点とヒント
Luaのtableは非常に強力で柔軟ですが、使う上で知っておくべきいくつかの注意点や、より効率的・安全に使うためのヒントがあります。
9.1 キーに使える型と使えない型
前述の通り、nilとNaNを除く全ての種類のデータをtableのキーとして使用できます。
-
使えるキーの例:
- 数値 (整数、浮動小数点数)
- 文字列
- 真偽値 (
true,false) - 関数
- userdata (C言語で作成されたカスタム型)
- 別のtable (table自身をキーにすることも可能)
-
使えないキー:
nil:nilは「値が存在しない」ことを意味するため、キーとして使うことはできません。table[key] = nilはキーの削除を意味しましたね。NaN(Not a Number):NaNは他のNaNと比較しても常にfalseになるという特殊な性質を持つため、tableのキーとして使用すると、キーの一意性を保証できなくなります。そのため、NaNもキーとして使えません。
9.2 キーの存在確認
特定のキーがtableに存在するかどうかを確認したい場合があります。これには、そのキーにアクセスした結果が nil かどうかをチェックする方法が最も一般的です。
“`lua
local person = { name = “Alice”, age = 30 }
if person.name ~= nil then
print(“Name exists.”)
end
if person.city == nil then — == nil で存在しないことを確認
print(“City does not exist.”)
end
— 変数をキーとして使う場合も同じ
local key_to_check = “age”
if person[key_to_check] ~= nil then
print(key_to_check .. ” exists.”)
end
“`
if person.some_key then ... end のようにシンプルに書くことも多いですが、もし some_key の値として false が格納されている可能性がある場合は注意が必要です。if 文では nil と false のみが偽と判断されるため、false が格納されているキーも「存在しない」かのように扱われてしまいます。
“`lua
local status = { is_active = false, message = “ok” }
if status.is_active then
print(“Active”) — 出力されない (falseだから)
else
print(“Not Active or Does Not Exist”)
end
— キーが存在するかどうかを正確にチェックするには、nilと比較するのが安全
if status.is_active ~= nil then
print(“is_active key exists.”) — 出力される
end
if status.last_login ~= nil then
print(“last_login key exists.”)
else
print(“last_login key does not exist.”) — 出力される
end
“`
したがって、キーの「存在」を確認したい場合は、table[key] ~= nil を使うのが最も正確です。
9.3 空のTable {} の挙動
空のtable {} は、作成された時点ではキーも値も何も持っていません。# 演算子を適用すると 0 を返します。
lua
local empty = {}
print(#empty) -- 出力: 0
しかし、後から要素を追加していくと、追加されたキーに応じて配列部分やハッシュ部分が内部的に構築されていきます。
“`lua
local t = {}
t[1] = “a” — 配列部分が始まる
t[2] = “b”
t.name = “Test” — ハッシュ部分が始まる
print(#t) — 出力: 2 (配列部分は2まで連続)
print(t.name) — 出力: Test
print(t[1]) — 出力: a
“`
9.4 メモリ効率に関する簡単な注意
Luaのtableは内部的に配列部分とハッシュ部分を持つため、使用するキーの種類によってメモリの使用効率が変わることがあります。
- 密な配列(dense array): 1から始まる連続した整数キーのみを使用し、途中に
nilがないようなtableは、配列部分が効率的に使われるため、メモリ効率が良い傾向があります。 - スパースな配列(sparse array): 整数キーを使うが、キーが飛び飛びになっている(例:
{[1]="a", [100]="b"})ようなtableは、ハッシュ部分が使われるため、密な配列ほど効率的ではない場合があります。 - ハッシュテーブルとしてのみ使う: 文字列キーやその他の非整数キーのみを使うtableも、ハッシュ部分が使われます。キーの種類や数によって効率は変動します。
通常の使用においては、この内部的な効率の違いをそこまで気にする必要はありません。LuaVMがうまく管理してくれます。しかし、非常に大きなデータを扱う場合や、パフォーマンスがクリティカルな部分では、キーの設計を工夫することでメモリ使用量やアクセス速度に影響を与える可能性はあります。例えば、大きな連続した数値インデックスが必要なら、それを効率的に扱えるように設計することが望ましいでしょう。
9.5 適切なキーの選択
どのようなキーを選ぶかは、tableの使い方に直結します。
- 順番に並んだデータの集まり: 配列として扱うのが自然です。1から始まる整数キーを使い、
#演算子、ipairs、table.insert/remove/sortを活用します。 - 名前で識別したいデータの集まり: 文字列キーを使う連想配列が適しています。ドット記法
.nameのような直感的なアクセスが可能になります。 - 特定のオブジェクトに関連付けたいデータ: そのオブジェクト自身をキーとして使うことも可能です。
tableはその柔軟性ゆえに、様々な設計の選択肢があります。どのようなキーと値のペアにするか、そしてそのtableをどのように利用したいかを明確にすることが、分かりやすく効率的なコードを書く上で重要です。
10. まとめ:TableはLuaの心臓部
この記事では、Luaの核となるデータ構造であるtableについて、その基本的な作成方法から、値のアクセス・変更・削除、配列や連想配列としての使い方、標準ライブラリ関数、イテレーションの方法、さらにはネスト、OOP、メタテーブルといった応用、そして参照とコピーの概念、注意点まで、幅広く詳細に解説しました。
Luaのtableは、他の言語であれば複数の異なるデータ型で表現される機能を一つに統合した、非常にユニークで強力な存在です。
- シンプル:
{}で作れ、.や[]でアクセスできる基本的な操作は非常に簡単です。 - 柔軟: 数値、文字列、真偽値、関数、tableなど、あらゆる型のキーと値を格納できます。配列としても辞書としても機能し、両者を混在させることも可能です。
- 強力: メタテーブルの仕組みと組み合わせることで、オブジェクト指向や演算子オーバーロードといった高度な機能も実現できます。
最初は「一つのもので何でもやる」ことに戸惑うかもしれません。しかし、配列として使うときは1から始まる整数キー、辞書として使うときは文字列キーなど、それぞれの「慣習的な使い方」を理解し、# や ipairs、pairs といった関連機能を使い分けることで、Luaのtableを効果的に活用できるようになります。
この記事が、あなたのLuaプログラミング学習におけるtable理解の強固な土台となることを願っています。tableはLuaの心臓部です。その鼓動を感じ取り、自由自在に操れるようになれば、Luaを使った開発がきっと楽しくなるはずです。
さあ、学んだことを活かして、実際にコードを書いてみましょう!