【Lua入門】配列の全て!仕組みから実践的な使い方まで


【Lua入門】配列の全て!仕組みから実践的な使い方まで

はじめに:Luaにおける配列の重要性とユニークな特徴

プログラミングにおいて、複数のデータをまとめて扱うための最も基本的な構造の一つが「配列」です。数値のリスト、名前のリスト、ゲームのアイテム一覧など、現実世界の様々な事物をプログラム上で表現する際に配列は欠かせません。

他の多くのプログラミング言語と同様に、Luaにも配列のようなデータ構造が存在します。しかし、Luaの配列は他の言語のそれとは少し異なるユニークな特徴を持っています。その最大の特徴は、Luaの配列が「テーブル」と呼ばれる単一の強力なデータ構造を用いて実現されているという点です。

本記事では、Luaの配列の正体である「テーブル」の仕組みから紐解き、配列の基本的な使い方、要素の追加・削除、繰り返し処理、そして標準ライブラリ table モジュールを使った実践的なテクニックまで、Luaにおける配列の全てを詳細かつ分かりやすく解説します。

Luaを始めたばかりで配列の扱いに戸惑っている方、Luaの配列が他の言語とどう違うのか知りたい方、そしてLuaの配列を使いこなしてより効率的なプログラミングを目指したい方、すべての方にとって役立つ内容となることを目指します。

1. Luaの配列の正体:万能データ構造「テーブル」

他の多くの言語では、配列(Array)、リスト(List)、辞書(Dictionary)やハッシュマップ(HashMap)、構造体(Struct)など、複数の異なるデータ構造を使い分けます。しかし、Luaにはこれらの機能をほとんど全て賄える「テーブル(table)」というただ一つの主要なデータ構造しかありません。

テーブルはLuaの核となるデータ構造であり、非常に強力で柔軟です。テーブルは連想配列(Associative Array)とも呼ばれ、キー(key)値(value)のペアの集合を保持します。キーには任意の型の値(nilNaNを除く)を使うことができ、値にも任意の型の値(nilを含む)を使うことができます。

Luaの「配列」は、このテーブルを特定の形式で利用した場合に、配列のように振る舞うものとして認識されるのです。具体的には、正の整数をキーとして使い、1から順に連続してキーが割り当てられているテーブルが、いわゆる「配列」として扱われます。

例えば、{"apple", "banana", "cherry"}というデータのリストを考えます。これをLuaのテーブルとして表現すると、内部的には以下のようなキーと値のペアの集合として扱われます。

  • キー: 1, 値: “apple”
  • キー: 2, 値: “banana”
  • キー: 3, 値: “cherry”

このように、キーが1から始まり、整数が順番に割り当てられているテーブルが、Luaにおける配列の実体です。

Luaの配列が持つユニークな特徴まとめ:

  1. テーブルによる実装: 配列は特別な型ではなく、テーブルの一つの使い方である。
  2. 1ベースインデックス: 他の多くの言語(C, Java, Pythonなど)では配列のインデックスは0から始まりますが、Luaでは1から始まります。これはLuaの設計思想によるもので、非プログラマーにも分かりやすくすることを意図しています。
  3. 動的なサイズ: 配列(テーブル)のサイズは固定されていません。要素を追加したり削除したりすることで、実行時に自由にサイズを変更できます。
  4. 疎な配列(Sparse Array)が可能: 必ずしも1から連続したインデックスである必要はありません。例えば、{10="ten", 20="twenty"} のようなテーブルも作成可能です。ただし、このような構造は通常の「配列」とは異なる挙動を示すことがあります(後述の長さ取得や繰り返し処理の項で詳しく解説します)。
  5. 任意の値を格納可能: 配列の要素には、数値、文字列、真偽値、他のテーブル、関数、nilなど、Luaのあらゆる型の値を格納できます。

これらの特徴を理解することが、Luaの配列を正しく使いこなすための第一歩となります。特に1ベースインデックスと、配列がテーブルであることによる動的な性質、そして疎な配列が可能であるという点は、他の言語経験者にとっては慣れが必要な部分です。

2. 基本的な配列の作成と初期化

Luaで配列を作成するには、テーブルを作成するための波括弧 {} を使用します。

2.1. 空の配列の作成

最も簡単な方法は、空の波括弧を使用することです。

lua
local myArray = {} -- 空のテーブル(配列としても利用可能)を作成

この myArray は、現時点では何も要素を持っていませんが、後から要素を追加していくことができます。

2.2. リテラルによる初期化

配列を作成と同時に初期値を設定するには、波括弧 {} の中に要素をカンマ , 区切りで並べます。

lua
local fruits = {"apple", "banana", "cherry"}

この記述方法は、内部的に以下のようなキーと値のペアを持つテーブルを作成します。

lua
-- 内部的にはこのように扱われる
-- { [1] = "apple", [2] = "banana", [3] = "cherry" }

キーを明示的に記述しない場合、Luaは自動的に1から始まる連続した整数をキーとして割り当てます。これがLuaで最も一般的な配列の初期化方法です。

もちろん、数値や他の型の値を混ぜて初期化することも可能です。

lua
local mixedArray = {123, "hello", true, nil, {a=1, b=2}}
-- この場合も、インデックスは1, 2, 3, 4, 5 と自動的に割り当てられる

2.3. 明示的なキーを使った初期化

連想配列としてテーブルを初期化する際に、キーを明示的に指定する方法があります。この方法を使っても、整数キーで初期化すれば配列として機能するテーブルを作成できます。

lua
local students = {
[1] = "Alice",
[2] = "Bob",
[3] = "Charlie"
}

この書き方は、前のリテラル初期化 {"Alice", "Bob", "Charlie"} と全く同じ結果になります。キーが連続した整数である限り、配列として扱われます。

整数キー以外のキーを指定した場合、それは配列の要素とは別に、テーブルの連想配列部分として扱われます。

lua
local mixedTable = {
[1] = "first element",
[2] = "second element",
["name"] = "example table", -- 文字列キー
[true] = "boolean key" -- 真偽値キー
}

この mixedTable は、キー 12 を持つ部分が配列的な要素として扱われる可能性がありますが、全体としては「配列と連想配列が混在したテーブル」という構造になります。Luaの配列に関する機能(特に長さ取得や特定の繰り返し処理)を使用する際は、この混在が予期しない挙動を引き起こす可能性があるため注意が必要です。一般的には、配列として使いたい場合は整数キーのみを使用し、連想配列として使いたい場合は文字列キーなどを使う、という使い分けが推奨されます。

3. 配列の要素へのアクセスと変更

配列(連続した整数キーを持つテーブル)の要素にアクセスするには、変数名の後に角括弧 [] を使い、その中にアクセスしたい要素のインデックス(キー)を指定します。

3.1. 要素の取得

“`lua
local colors = {“red”, “green”, “blue”}

print(colors[1]) — 出力: red (1番目の要素)
print(colors[2]) — 出力: green (2番目の要素)
print(colors[3]) — 出力: blue (3番目の要素)
“`

重要な注意点: Luaのインデックスは1から始まります。colors[0] は存在しませんし、 colors[4] も現時点では存在しません。

存在しないインデックスにアクセスした場合、Luaはエラーではなく nil を返します。

lua
local numbers = {10, 20, 30}
print(numbers[4]) -- 出力: nil
print(numbers[0]) -- 出力: nil
print(numbers["hello"]) -- 出力: nil (文字列キーも存在しない)

この nil が返るという性質は、要素が存在するかどうかをチェックする際に役立ちます。

lua
if colors[5] == nil then
print("5番目の要素は存在しません。")
end

3.2. 要素の変更

特定のインデックスの要素の値を変更するには、そのインデックスを指定して新しい値を代入します。

“`lua
local animals = {“cat”, “dog”, “elephant”}

animals[2] = “fox” — 2番目の要素を “dog” から “fox” に変更
print(animals[1]) — 出力: cat
print(animals[2]) — 出力: fox
print(animals[3]) — 出力: elephant
“`

存在しないインデックスに対して代入を行うと、そのインデックスを持つ新しい要素が追加されます。

“`lua
local items = {“sword”, “shield”}

items[3] = “potion” — 3番目の要素を追加
print(items[3]) — 出力: potion

items[5] = “armor” — 5番目の要素を追加 (4番目はまだnil)
print(items[5]) — 出力: armor
print(items[4]) — 出力: nil
“`

このように、存在しないインデックスに代入することで、配列のサイズを動的に拡張できます。ただし、上記の例のように間に歯抜け(nilのインデックス)ができると、それはもはや「連続した整数キーを持つテーブル」ではなくなり、「疎な配列(Sparse Array)」や「配列と連想配列の混在」として扱われるようになります。この状態での長さ取得や繰り返し処理には注意が必要です(後述)。

3.3. 要素の削除(値をnilにする)

特定の要素を削除したい場合は、そのインデックスに nil を代入します。

“`lua
local list = {“A”, “B”, “C”, “D”}

list[2] = nil — 2番目の要素 “B” を削除(nilにする)

print(list[1]) — 出力: A
print(list[2]) — 出力: nil
print(list[3]) — 出力: C
print(list[4]) — 出力: D
“`

この操作によって、インデックス 2 の場所は nil になります。要素が物理的に詰められるわけではなく、「穴が開く」イメージです。この場合、インデックス 1, 3, 4 は存在しますが、インデックス 2nil となり、連続性が途切れます。これもまた「疎な配列」の状態を作り出します。

連続性を保ったまま要素を削除して詰めたい場合は、後述する table.remove() 関数を使用する必要があります。

4. 配列の長さ(要素数)の取得:#演算子と注意点

配列の長さ(要素数)を取得するには、長さ演算子である # を使用します。

lua
local data = {"apple", "banana", "cherry", "date"}
print(#data) -- 出力: 4

これは非常に便利ですが、Luaの # 演算子には重要な注意点があります。それは、# 演算子が返す長さが、「1から数えて、値がnilでない要素が連続している最後のインデックス」であるという点です。

もう少し正確に言うと、Luaの # 演算子(テーブルに対して使用した場合)は、テーブルが配列として使われていると仮定し、インデックス 1 から順に見ていったときに、最初の nil 値が見つかる直前のインデックスを返します。もしテーブルが完全に連続した整数キーを持つ密な配列であれば、これは配列の実際の要素数と一致します。

しかし、配列に「穴」(途中に nil のインデックス)が開いている場合や、配列の末尾に nil を代入した場合、# 演算子は期待通りの値を返さないことがあります。

“`lua
local sparseArray = {“A”, “B”, nil, “D”, “E”} — インデックス 3 が nil

print(#sparseArray) — 出力: 2
— 1から順に見ていくと、sparseArray[1]は”A”, sparseArray[2]は”B”, sparseArray[3]はnil です。
— 最初のnilが見つかったのはインデックス3なので、その直前のインデックス2が長さとして返されます。
“`

もう一つの例を見てみましょう。

“`lua
local mixedArray = {10, 20, 30, [5]=”fifty”, [6]=”sixty”}
— インデックス 1, 2, 3 は連続しているが、インデックス 4 が nil(存在しない)

print(#mixedArray) — 出力: 3
— 1から順に見て、mixedArray[1]=10, mixedArray[2]=20, mixedArray[3]=30 ですが、
— mixedArray[4] は nil(存在しない)です。最初のnilはインデックス4なので、その直前のインデックス3が長さとして返されます。
— インデックス5や6に値が入っていても、#演算子には影響しません。
“`

このように、# 演算子はあくまで「1から始まる連続した非nil要素の長さ」を返すものであり、テーブルに存在する全てのキー・バリューペアの総数を返すわけではありません。

テーブル全体のキー数を正確に数える方法

もし、途中に nil があろうと、あるいは文字列キーなどが混じっていようと、テーブルが持つ全てのキー・バリューペアの総数を正確に知りたい場合は、繰り返し処理を使って数える必要があります。

lua
local totalCount = 0
for key, value in pairs(mixedTable) do -- pairsイテレータを使用
totalCount = totalCount + 1
end
print("テーブル全体の要素数:", totalCount) -- mixedTableの定義によるが、例えば5が出力される可能性がある

pairs() イテレータはテーブルの全てのキー・バリューペアを走査するため、この方法で正確な総数を取得できます。ただし、これはテーブルのサイズに比例した処理時間が必要になるため、頻繁に呼び出すとパフォーマンスに影響を与える可能性があります。通常、配列として密に使っている場合は # 演算子で十分であり、効率的です。疎な配列や連想配列が混じっている場合は、その構造と目的に応じて # または pairs による走査を使い分けましょう。

5. 配列の要素の追加と削除:tableモジュール

配列の要素を追加したり削除したりするには、手動でインデックスに代入する方法の他に、Luaの標準ライブラリ table モジュールに含まれる便利な関数を使用する方法があります。これらの関数は、配列の連続性を保つように要素を詰めたり広げたりする処理を自動的に行ってくれるため、配列をリストのように扱う場合に非常に便利です。

5.1. 要素の追加

要素を追加する主な方法は以下の2つです。

  1. 末尾への追加: table.insert(array, value)
    # 演算子で取得できる現在の配列の「長さ」の次のインデックスに value を追加します。

    “`lua
    local items = {“sword”, “shield”}
    table.insert(items, “potion”) — 末尾に追加 (インデックス 3)

    print(#items) — 出力: 3
    print(items[3]) — 出力: potion
    ``
    これは、
    items[#items + 1] = “potion”と書くのとほぼ同じ効果がありますが、table.insert` を使う方が慣習的です。

  2. 指定位置への挿入: table.insert(array, index, value)
    指定した index の位置に value を挿入し、その位置にあった要素やそれ以降の要素を一つずつ後ろ(より大きいインデックス)にずらします。

    “`lua
    local numbers = {10, 30, 40}
    table.insert(numbers, 2, 20) — インデックス 2 の位置に 20 を挿入

    print(numbers[1]) — 出力: 10
    print(numbers[2]) — 出力: 20 (元々の 30 が後ろにずれた)
    print(numbers[3]) — 出力: 30 (元々の 40 が後ろにずれた)
    print(numbers[4]) — 出力: 40 (新しく追加された要素)
    print(#numbers) — 出力: 4
    “`

    table.insert は、既存の要素を自動的にシフトするため、配列の連続性を維持したまま要素を挿入したい場合に非常に便利です。指定する index は 1 から #array + 1 の範囲で有効です。index に 1 を指定すると配列の先頭に挿入され、#array + 1 を指定すると末尾に追加されます(引数が2つの table.insert と同じ挙動になります)。

    “`lua
    local letters = {“b”, “c”}
    table.insert(letters, 1, “a”) — 先頭に挿入
    print(letters[1], letters[2], letters[3]) — 出力: a b c

    table.insert(letters, #letters + 1, “d”) — 末尾に挿入(これは table.insert(letters, “d”) と同じ)
    print(letters[1], letters[2], letters[3], letters[4]) — 出力: a b c d
    “`

5.2. 要素の削除

要素を削除するには table.remove() 関数を使用します。

  1. 末尾からの削除: table.remove(array)
    # 演算子で取得できる現在の配列の「長さ」の位置にある要素を削除し、その値を返します。

    “`lua
    local queue = {“first”, “second”, “third”}
    local removedElement = table.remove(queue) — 末尾の “third” を削除

    print(removedElement) — 出力: third
    print(#queue) — 出力: 2
    print(queue[3]) — 出力: nil (削除された)
    “`

  2. 指定位置からの削除: table.remove(array, index)
    指定した index の位置にある要素を削除し、その位置より後ろにあった要素を一つずつ前(より小さいインデックス)にずらします。削除された要素の値を返します。

    “`lua
    local playlist = {“Song A”, “Song B”, “Song C”, “Song D”}
    local removedSong = table.remove(playlist, 2) — インデックス 2 の “Song B” を削除

    print(removedSong) — 出力: Song B
    print(playlist[1]) — 出力: Song A
    print(playlist[2]) — 出力: Song C (元々の “Song C” が前にずれた)
    print(playlist[3]) — 出力: Song D (元々の “Song D” が前にずれた)
    print(playlist[4]) — 出力: nil (元の末尾が前にずれて、インデックス 4 は空になった)
    print(#playlist) — 出力: 3
    “`

    table.remove(array, index) は、要素を削除した後に自動的に要素をシフトしてくれるため、削除後も配列の連続性を保つことができます。指定する index は 1 から #array の範囲で有効です。

table.insert および table.remove は、大量の要素に対して頻繁に実行すると、要素のシフト処理のためにパフォーマンスが低下する可能性があります。特に配列の先頭に近い位置での挿入/削除は、より多くの要素のシフトが必要になるため、処理コストが高くなります。配列の末尾での操作は、シフトする要素が少ない(または無い)ため、比較的効率的です。

6. 配列の走査(イテレーション)

配列の要素を順番に処理するには、いくつかの方法があります。Luaでは、主に数値forループ、ipairs() イテレータ、そして pairs() イテレータが使われます。それぞれの特徴と使い分けを理解することが重要です。

6.1. 数値forループ

最も基本的な方法は、数値インデックスを使って for ループを回す方法です。

“`lua
local fruits = {“apple”, “banana”, “cherry”}

for i = 1, #fruits do — 1から配列の長さまでループ
print(“インデックス”, i, “:”, fruits[i])
end
— 出力:
— インデックス 1 : apple
— インデックス 2 : banana
— インデックス 3 : cherry
“`

この方法は、インデックスが1から連続している「密な配列」に対してはうまく機能します。しかし、配列に「穴」(nilの要素)がある場合、# 演算子が正確な長さを返さないため、ループの範囲が途中で終わってしまう可能性があります。

“`lua
local sparseArray = {“A”, “B”, nil, “D”, “E”} — #sparseArray は 2 を返す

for i = 1, #sparseArray do — ループは 1 から 2 までしか回らない
print(“インデックス”, i, “:”, sparseArray[i])
end
— 出力:
— インデックス 1 : A
— インデックス 2 : B
— インデックス 3, 4, 5 の要素は処理されない
“`

したがって、数値forループは、インデックスが1から終端まで完全に連続していることが保証されている配列に対して使うのが最も安全です。

6.2. ipairs() イテレータ

Luaの標準ライブラリには、テーブルを走査するための組み込みイテレータがいくつかあります。その一つが ipairs() です。ipairs() は、テーブルを配列のように、すなわち1から始まる連続した整数キーのみを走査します。

“`lua
local colors = {“red”, “green”, “blue”}

for index, value in ipairs(colors) do
print(“インデックス”, index, “:”, value)
end
— 出力:
— インデックス 1 : red
— インデックス 2 : green
— インデックス 3 : blue
“`

ipairs() の重要な特徴は、走査中に nil の要素を見つけると、そこで走査を停止するという点です。これは # 演算子の挙動と似ています。

“`lua
local sparseArray = {“A”, “B”, nil, “D”, “E”}

for index, value in ipairs(sparseArray) do
print(“インデックス”, index, “:”, value)
end
— 出力:
— インデックス 1 : A
— インデックス 2 : B
— インデックス 3 は nil なので、ここでループが停止する。
— インデックス 4, 5 の要素は処理されない。
“`

この挙動から、ipairs()「1から始まる連続した配列部分」を走査したい場合に最も適しています。途中に nil がある疎な配列の場合、ipairs はその穴の手前までしか走査しません。また、文字列キーなどの非整数キーを持つ要素は ipairs では走査されません。

6.3. pairs() イテレータ

もう一つの主要なイテレータは pairs() です。pairs() は、テーブルが持つ全てのキー・バリューペアを走査します。これには、整数キー、文字列キー、その他の型のキー、そして値が nil でない全てのペアが含まれます。

“`lua
local mixedTable = {
[1] = “first”,
[2] = “second”,
[“name”] = “example”,
[true] = “boolean value”,
[5] = “fifth” — インデックスが飛んでいる
}

for key, value in pairs(mixedTable) do
print(“キー:”, tostring(key), “値:”, tostring(value)) — キーの型は様々なので tostring が便利
end
“`

この pairs() を使ったループの出力順序は、テーブルの実装に依存するため、特定の順番(例えば、数値インデックスの昇順)が保証されません。(C-Luaの場合、多くの場合インデックス順になることが多いですが、仕様として保証されているわけではありません。特に文字列キーなどを含む場合は順不同になります)。

pairs() は、テーブル全体の内容を確認したい場合や、連想配列としてテーブルを使っている場合に適しています。配列として使っている場合でも、疎な配列で途中の nil を無視して存在する全ての数値インデックス要素を処理したい場合(ただし順序は保証されない)、あるいは数値インデックス以外の要素も一緒に処理したい場合に使用します。

どのイテレータを選ぶべきか?

  • 密な配列(1から#arrayまで連続した整数キーを持つ):
    • 要素とインデックス両方が必要な場合: for i = 1, #array do ... array[i] ... end または for index, value in ipairs(array) do ... end。どちらもほぼ同等の結果になりますが、ipairs の方がLuaのイテレータとしてより汎用的です。
    • 要素のみが必要でインデックスは不要な場合: for index, value in ipairs(array) do ... value ... end
  • 疎な配列(途中にnilがある)や、配列的な部分と連想配列的な部分が混在したテーブル:
    • 1から始まる連続した配列部分のみを処理したい(最初のnilで止まる): ipairs()
    • テーブル内の全てのキー・バリューペアを処理したい(順序は保証されない): pairs()

一般的に、数値インデックスの連続性を利用したい場合は ipairs が推奨されます。テーブル全体(配列部分+連想配列部分)を走査したい場合は pairs を使います。

7. 多次元配列

Luaには厳密な意味での「多次元配列」という専用の構文はありません。しかし、テーブルの中にさらにテーブルを格納することで、他の言語における多次元配列や行列のような構造を表現できます。これは「テーブルのテーブル」と考えるのが最も分かりやすいでしょう。

例えば、2×3の行列を作成するには、3つの要素を持つテーブルを2つ含むテーブルを作成します。

lua
local matrix = {
{1, 2, 3}, -- 1行目 (インデックス 1)
{4, 5, 6} -- 2行目 (インデックス 2)
}

この matrix は、2つのテーブル要素 {1, 2, 3}{4, 5, 6} を持つテーブルです。

要素にアクセスするには、インデックスを続けて指定します。matrix[i][j] の形式です。

“`lua
— 1行目の要素にアクセス
print(matrix[1][1]) — 出力: 1
print(matrix[1][2]) — 出力: 2
print(matrix[1][3]) — 出力: 3

— 2行目の要素にアクセス
print(matrix[2][1]) — 出力: 4
print(matrix[2][2]) — 出力: 5
print(matrix[2][3]) — 出力: 6
“`

要素を変更するのも同様です。

lua
matrix[1][2] = 99
print(matrix[1][2]) -- 出力: 99

多次元配列を走査するには、ネストしたループを使用します。通常は ipairs または数値forループを使います。

“`lua
local rows = #matrix — 行数 (外側のテーブルの長さ)
local cols = #matrix[1] — 列数 (1行目のテーブルの長さと仮定)

for i = 1, rows do
for j = 1, cols do
— または for j, value in ipairs(matrix[i]) do … end を使う
print(string.format(“matrix[%d][%d] = %d”, i, j, matrix[i][j]))
end
end
“`

この方法で、3次元以上の配列も同様に table[i][j][k] のように表現できます。

ただし、各行(内側のテーブル)が常に同じ長さを持つとは限りませんし、途中の行が nil である可能性もあります。より複雑な構造や疎な多次元データを扱う場合は、そのデータ構造に合わせて pairs を使う必要があるかもしれません。しかし、一般的な行列やグリッド構造であれば、上記のように ipairs や数値forループを使った「テーブルのテーブル」表現で十分です。

8. table標準ライブラリの便利な関数

既に table.insert()table.remove() を紹介しましたが、table モジュールには配列操作に役立つ他の関数も用意されています。

8.1. table.concat (list [, sep [, i [, j]]])

配列の要素を結合して一つの文字列を作成します。

  • list: 結合したいテーブル(配列)
  • sep: 要素間に挿入する区切り文字(省略可能、デフォルトは空文字列 ""
  • i: 結合を開始するインデックス(省略可能、デフォルトは 1)
  • j: 結合を終了するインデックス(省略可能、デフォルトは #list

結合される要素は、文字列または数値である必要があります(それ以外の型はエラーになります)。

“`lua
local words = {“hello”, “world”, “lua”}
local sentence = table.concat(words, ” “) — 要素間にスペースを挟んで結合
print(sentence) — 出力: hello world lua

local numbers = {10, 20, 30, 40, 50}
local part = table.concat(numbers, “-“, 2, 4) — インデックス 2 から 4 までを “-” で結合
print(part) — 出力: 20-30-40

local mixed = {“a”, 1, “b”, 2}
print(table.concat(mixed)) — 出力: a1b2
“`

table.concat は、ログメッセージの組み立てや、CSVのようなデータ形式の生成など、配列の要素を効率的に文字列化したい場合に非常に便利です。

8.2. table.sort (list [, func])

配列の要素をソート(並べ替え)します。

  • list: ソートしたいテーブル(配列)。ソートは元のテーブルに対して直接行われます(in-place)。
  • func: 比較関数(省略可能)。2つの引数 ab を受け取り、ab より「前」に来るべき場合に true を返す関数を指定します。省略した場合、Luaの標準的な < 演算子(数値順、文字列のアルファベット順)が使用されます。

“`lua
local numbers = {5, 2, 8, 1, 9, 4}
table.sort(numbers) — 昇順にソート(デフォルトの比較)
for _, v in ipairs(numbers) do print(v) end
— 出力: 1 2 4 5 8 9

local names = {“Charlie”, “Alice”, “Bob”}
table.sort(names) — アルファベット順にソート(デフォルトの比較)
for _, v in ipairs(names) do print(v) end
— 出力: Alice Bob Charlie

local scores = {100, 75, 90, 80}
— 降順にソートするための比較関数
local function compareDescending(a, b)
return a > b — a が b より大きい場合に true を返す
end
table.sort(scores, compareDescending)
for _, v in ipairs(scores) do print(v) end
— 出力: 100 90 80 75
“`

table.sort は、数値インデックスが1から #list まで連続している配列に対してのみ正しく機能します。途中に nil があるテーブルや、文字列キーが含まれるテーブルに対して使用した場合の挙動は未定義です(通常はエラーになるか、予期しない結果になります)。ソート後も、結果は1から始まる連続した整数キーを持つ配列となります。

table.sort の比較関数は、数値だけでなく、他のデータ型の比較にも使えます。例えば、テーブルのリストを特定のキーの値でソートすることも可能です。

“`lua
local people = {
{name = “Alice”, age = 30},
{name = “Bob”, age = 25},
{name = “Charlie”, age = 35}
}

— ageで昇順にソート
table.sort(people, function(p1, p2)
return p1.age < p2.age
end)

for _, person in ipairs(people) do
print(person.name, person.age)
end
— 出力:
— Bob 25
— Alice 30
— Charlie 35
“`

table.sort は非常に柔軟で強力な関数であり、Luaでリストデータを扱う際には頻繁に利用されます。

9. 実践的な使い方と注意点

9.1. 配列 vs 連想配列(テーブル)の使い分け

Luaのテーブルは配列としても連想配列としても使えますが、その使い分けは重要です。

  • 配列として使う場合: データの順序が重要で、1から始まる連続した整数インデックスで要素にアクセスしたい場合(リスト、スタック、キュー、数値列など)。table.inserttable.removetable.sortipairs といった関数/イテレータが効率的に機能します。
  • 連想配列として使う場合: キーを使って特定のデータに名前でアクセスしたい場合(設定値、レコード、オブジェクトのプロパティなど)。キーは文字列であることが多いですが、数値や他の型のキーも使えます。pairs イテレータが全てのキーを走査するために使われます。

一つのテーブルの中で配列的な部分(整数キー)と連想配列的な部分(文字列キーなど)を混在させることも可能ですが、これはしばしば混乱の原因となり、# 演算子や ipairs の挙動を予測しにくくします。特別な理由がない限り、役割に応じてテーブルを分けることを推奨します。

“`lua
local config = { — 連想配列として使う
windowTitle = “My Application”,
screenWidth = 800,
screenHeight = 600,
fullscreen = false
}

local playerInventory = { — 配列として使う
“sword”,
“shield”,
“potion”,
“map”
}
“`

9.2. 1ベースインデックス vs 0ベースインデックス

他の多くの言語(C, C++, Java, C#, Python, JavaScriptなど)では配列のインデックスは0から始まります。これに対してLuaは1から始まります。これはLuaの設計思想によるものですが、他の言語経験者にとっては慣れるまでオフバイワンエラー(インデックスのずれによる間違い)の原因になりやすい点です。

  • ループの開始・終了条件 (for i = 1, #array do)
  • 要素へのアクセス (array[1] は最初の要素)
  • table.insert, table.remove のインデックス

これらの操作を行う際は、常に「Luaのインデックスは1から始まる」ということを意識しましょう。特に他の言語のコードやアルゴリズムをLuaに移植する際には注意が必要です。

9.3. 疎な配列と密な配列

前述のように、Luaの配列は途中に nil の要素が含まれる「疎な配列」になる可能性があります。

lua
local sparse = {10, 20, nil, 40}
print(#sparse) -- 出力: 2 (最初のnilの手前まで)

密な配列(1から #array まで全てのインデックスに非nilの値が入っている配列)は、# 演算子や ipairs が期待通りに機能し、table.insert/table.remove も効率的に動作します。一方、疎な配列ではこれらの挙動が変わるため、意図しないバグの原因になる可能性があります。

配列として扱う場合は、可能な限り密な配列を維持するように心がけましょう。要素を削除する際は、単に nil を代入するのではなく table.remove() を使って要素を詰めるのが一般的です。

9.4. 大きな配列を扱う際のメモリ効率

Luaのテーブルは動的であり、内部的にはハッシュテーブルと配列部分を組み合わせて実装されています。密な配列の場合、内部的には連続したメモリ領域に要素が配置されるように最適化される傾向があります(特にLuaJIT)。しかし、非常に大きな配列や極端に疎な配列を扱う場合、メモリ使用量やアクセス速度が問題になる可能性があります。

もし数百万、数千万といった単位の大量の数値を扱う必要がある場合は、Luaのテーブル配列よりも、特定のライブラリ(例: FFIを使ってCの配列を直接扱う、数値計算ライブラリなど)の方が効率的な場合があります。しかし、一般的なアプリケーション開発においては、Luaのテーブル配列で十分なパフォーマンスが得られることがほとんどです。

9.5. 関数呼び出し時の引数としての配列

Luaでは、関数に複数の値を渡す際、通常はコンマ区切りで引数を並べます。しかし、一つのテーブル(配列)として複数の値をまとめて渡すというパターンもよく使われます。

“`lua
local function processData(data)
— data はテーブルとして渡される
for i, value in ipairs(data) do
print(“処理中:”, value)
end
end

local myData = {“item1”, “item2”, “item3”}
processData(myData) — テーブルを引数として渡す
“`

また、関数は複数の戻り値を返すことができますが、これをテーブルにまとめて受け取ることも可能です。

“`lua
local function getCoordinates()
return 100, 200 — 複数の戻り値
end

— 複数の変数で受け取る
local x, y = getCoordinates()

— テーブルにまとめて受け取る
local coords = {getCoordinates()} — {100, 200} という配列になる
print(coords[1], coords[2]) — 出力: 100 200
“`

特に、可変長の引数を受け取る関数(... を使う関数)にテーブルの内容を展開して渡したい場合は、unpack() または table.unpack() 関数を使用します。

“`lua
local function sum(…)
local total = 0
— 可変引数をテーブルに格納し、ipairsで合計する例
local args = table.pack(…) — Lua 5.2 以降推奨
for i = 1, args.n do
total = total + args[i]
end
return total

— または unpack を使う(Lua 5.1 まで、または Lua 5.2+ では table.unpack)
— for i, v in ipairs({unpack(arg)}) do total = total + v end — arg は可変引数をテーブルとして受け取る変数(非推奨)
end

local numbers_to_sum = {1, 2, 3, 4, 5}
print(sum(unpack(numbers_to_sum))) — unpack または table.unpack でテーブル要素を個別の引数に展開
— Lua 5.2 以降では table.unpack を使う方がより安全で推奨されます
— print(sum(table.unpack(numbers_to_sum)))
— 出力: 15
“`

unpack または table.unpack は、テーブルの要素を順番に、関数の引数として展開します。これも配列を操作する上での重要なテクニックです。

10. よくある間違いとその対策

10.1. インデックスのオフバイワンエラー

これは他の言語経験者が最も犯しやすい間違いです。Luaのインデックスは1から始まります!

“`lua
local arr = {“a”, “b”, “c”}
— 他の言語では arr[0] が最初の要素だが、Luaでは arr[1]
print(arr[0]) — 出力: nil (間違ったアクセス)
print(arr[1]) — 出力: a (正しい最初の要素へのアクセス)

— ループも 1 から開始
for i = 0, #arr – 1 do — 間違い!
— …
end
for i = 1, #arr do — 正しい!
— …
end
“`

対策: 常に「Luaのインデックスは1から始まる」ことを意識し、特にループの開始・終了条件や要素へのアクセス時には注意深くインデックスを確認する習慣をつけましょう。

10.2. #演算子の誤解によるループのバグ

# 演算子が、配列に「穴」がある場合に期待通りの長さを返さないことを知らないと、ループが途中で終了してしまうなどのバグにつながります。

lua
local data = {"A", "B", nil, "D"}
print(#data) -- 出力: 2
for i = 1, #data do
print(data[i]) -- "A", "B" までしか出力されない
end

対策: 配列が完全に密であることが保証されている場合のみ、# 演算子を使った数値forループや ipairs を使いましょう。途中に nil が存在する可能性がある、あるいは連想配列的なキーも含まれる場合は、テーブル全体を走査する pairs を使うことを検討してください(ただし順序は保証されません)。

10.3. ipairspairsの使い分けミス

ipairs は1から始まる連続した整数キーのみを走査し、最初の nil で停止します。pairs は全てのキー・バリューペアを走査しますが、順序は保証されません。これを混同すると、要素の一部が見落とされたり、処理順序が不定になったりします。

“`lua
local mixed = { [1]=”one”, [3]=”three”, [“name”]=”mixed” }
for k, v in ipairs(mixed) do print(“ipairs”, k, v) end
— 出力: ipairs 1 one (キー 2 が nil なので停止)

for k, v in pairs(mixed) do print(“pairs”, k, v) end
— 出力例 (順序は実行環境により異なる):
— pairs 1 one
— pairs name mixed
— pairs 3 three
“`

対策:
* 連続した配列部分を順序通りに処理したい → ipairs
* テーブル内の全ての要素(数値キー、文字列キーなど全て)を処理したい → pairs
目的に応じて適切なイテレータを選びましょう。

10.4. table.removeによるインデックスのずれ

table.remove(array, index) は指定したインデックスの要素を削除し、後続の要素を前にずらします。これは便利ですが、ループ中に table.remove を使う際に注意が必要です。

lua
local items = {"itemA", "itemB", "itemC", "itemD"}
-- ループしながら特定の条件で要素を削除したい場合の間違い
for i = 1, #items do
-- 例: "itemB" を削除したい
if items[i] == "itemB" then
table.remove(items, i)
-- 問題: インデックス i の要素を削除すると、元の items[i+1] が新しい items[i] になります。
-- 次のループで i がインクリメントされると、新しい items[i] (元の items[i+1]) がスキップされてしまいます。
end
end
-- 期待: itemA, itemC, itemD
-- 実際の可能性: itemA, itemD (itemC がスキップされる)

対策:
* 後ろから前に向かってループする: for i = #items, 1, -1 do ... table.remove(items, i) ... end
後ろから削除する場合、要素を削除してもそれより前のインデックスには影響がないため安全です。
* 要素を削除したらインデックスをデクリメントする: for i = 1, #items do if condition then table.remove(items, i); i = i - 1 end end
ただし、この方法はループ変数を手動で操作する必要があり、少し複雑になります。後ろからのループの方が推奨されます。

11. LuaJITにおける配列の最適化

Luaの標準実装(C-Lua)でも密な配列は効率的に扱われますが、高性能なJust-In-TimeコンパイラであるLuaJITを使用する場合、密な配列はさらに特別な最適化の対象となります。

LuaJITは、テーブルが「配列らしい」(1から始まる連続した整数キーのみを持ち、キー空間に隙間がない)と判断した場合、その部分をC言語の配列のように、連続したメモリブロックに格納するようにコンパイル時や実行時に最適化します。これにより、配列要素へのアクセスが非常に高速になり、他の言語の配列と同等かそれ以上のパフォーマンスを発揮する可能性があります。

この最適化の恩恵を最大限に受けるためには、以下の点を意識することが重要です。

  • 密な配列を維持する: 1から始まる連続した整数キーのみを使用し、途中に nil の要素を作らないようにします。
  • 連想配列的なキーと混在させない: 文字列キーなど、配列的な部分と連想配列的な部分を同じテーブルに混在させると、LuaJITが配列として最適化しにくくなる可能性があります。
  • ipairsや数値forループを使う: 配列として最適化されたテーブルを走査する際には、ipairsや数値forループを使う方が、pairsを使うよりもパフォーマンスが向上することが期待できます。

LuaJITを利用している環境では、密な配列を効果的に活用することが、コード全体のパフォーマンス向上に繋がる可能性があります。

12. まとめ:Lua配列使いこなしの要点

本記事では、Luaの配列について、その基本から応用までを詳しく見てきました。最後に、Lua配列を使いこなすための要点を改めて確認しておきましょう。

  1. Luaの配列はテーブルです: 配列は特別な型ではなく、1から始まる連続した整数キーを持つテーブルの一つの使い方です。テーブルの持つ柔軟性(動的なサイズ、異なるデータ型)がそのまま配列の特性になります。
  2. 1ベースインデックス: インデックスは常に1から始まります。他の言語経験者は特にこの点に注意が必要です。
  3. #演算子とipairsの挙動を理解する: これらの機能は「1から始まる連続した非nil要素の範囲」に依存します。途中に nil がある「疎な配列」では、期待通りに機能しない可能性があることを覚えておきましょう。
  4. pairsはテーブル全体を走査する: 整数キーだけでなく、文字列キーなど全てのキーを走査できますが、順序は保証されません。
  5. tableモジュールを活用する: table.inserttable.removetable.concattable.sort など、tableモジュールには配列操作に便利な関数が揃っています。特に insertremove は、配列の連続性を保ったまま要素を追加・削除するために重要です。
  6. 多次元配列はテーブルのテーブルとして表現する: Luaに専用の構文はありませんが、テーブルをネストさせることで簡単に実現できます。
  7. 密な配列を意識する(特にLuaJIT): パフォーマンスを考慮する場合、1から連続した整数キーのみを持つ密な配列としてテーブルを使用すると、最適化の恩恵を受けやすくなります。
  8. 使い分けが重要: 配列として使う場合(順序が必要、整数キー中心)と、連想配列として使う場合(キーによるアクセス、文字列キー中心)で、テーブルの使い方や使うべき関数/イテレータが変わります。混乱を避けるため、可能な限り役割に応じてテーブルを分けましょう。

Luaの配列(テーブル)は非常に柔軟で強力なツールです。最初は他の言語との違いに戸惑うかもしれませんが、これらの基本的な仕組みと注意点を理解すれば、Luaでのプログラミングがより効率的かつスムーズになるはずです。

本記事が、皆さんのLua学習の一助となれば幸いです。配列をマスターして、Luaでのプログラミングを楽しんでください!

付録:サンプルコード集

ここでは、本記事で解説した内容を網羅するいくつかのコード例を示します。

配列の作成と基本操作

“`lua
— 空の配列を作成
local emptyArray = {}

— リテラルで初期化
local items = {“sword”, “shield”, “potion”}

— 明示的なキーで初期化 (結果は上記と同じ)
local itemsExplicit = {[1]=”sword”, [2]=”shield”, [3]=”potion”}

— 要素へのアクセス
print(“最初の要素:”, items[1])
print(“2番目の要素:”, items[2])

— 要素の変更
items[2] = “armor”
print(“変更後の2番目の要素:”, items[2])

— 要素の追加 (存在しないインデックスへの代入)
items[4] = “helmet”
print(“追加後の要素:”, items[4])

— 要素の削除 (nilを代入)
items[3] = nil
print(“削除後の3番目の要素:”, items[3]) — 出力: nil
print(“現在のアイテムリスト (疎な配列):”, items[1], items[2], items[3], items[4]) — 出力: sword armor nil helmet

— #演算子による長さ取得
print(“現在の配列の長さ (#演算子):”, #items) — 出力: 2 (nilの手前まで)
“`

tableモジュールによる要素操作

“`lua
local list = {“apple”, “banana”, “cherry”}

— 末尾に要素を追加
table.insert(list, “date”)
print(“末尾に追加:”, list[1], list[2], list[3], list[4]) — 出力: apple banana cherry date

— 指定位置(インデックス 2)に要素を挿入
table.insert(list, 2, “blueberry”)
print(“指定位置に挿入:”, list[1], list[2], list[3], list[4], list[5]) — 出力: apple blueberry banana cherry date

— 末尾の要素を削除
local last = table.remove(list)
print(“末尾を削除:”, last) — 出力: date
print(“削除後:”, list[1], list[2], list[3], list[4], list[5]) — 出力: apple blueberry banana cherry nil

— 指定位置(インデックス 3)の要素を削除
local third = table.remove(list, 3)
print(“指定位置を削除:”, third) — 出力: banana
print(“削除後:”, list[1], list[2], list[3], list[4]) — 出力: apple blueberry cherry nil
“`

配列の走査

“`lua
local data = {“one”, “two”, “three”, “four”, “five”}
local sparseData = {“A”, “B”, nil, “D”, “E”}
local mixedTable = { [1]=”array1″, [2]=”array2″, [“name”]=”mixed”, [5]=”array5″ }

print(“\n— 数値forループ (密な配列) —“)
for i = 1, #data do
print(string.format(“data[%d] = %s”, i, data[i]))
end

print(“\n— 数値forループ (#の注意点) —“)
for i = 1, #sparseData do — #sparseData は 2
print(string.format(“sparseData[%d] = %s”, i, sparseData[i]))
end

print(“\n— ipairs (連続部分のみ, nilで停止) —“)
for index, value in ipairs(sparseData) do
print(string.format(“ipairs: sparseData[%d] = %s”, index, value))
end

print(“\n— ipairs (mixedTable) —“)
for index, value in ipairs(mixedTable) do — キー 3 が nil なので停止
print(string.format(“ipairs: mixedTable[%d] = %s”, index, value))
end

print(“\n— pairs (全てのキー, 順序不定) —“)
for key, value in pairs(mixedTable) do
print(string.format(“pairs: mixedTable[%s] = %s”, tostring(key), tostring(value)))
end
“`

多次元配列

“`lua
local matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}

print(“\n— 多次元配列のアクセス —“)
print(matrix[2][3]) — 出力: 6
matrix[1][1] = 10
print(matrix[1][1]) — 出力: 10

print(“\n— 多次元配列の走査 —“)
local rows = #matrix
for i = 1, rows do
local cols = #matrix[i] — 各行の長さは異なる可能性もあるため、内側ループで長さを取得するのが安全
for j = 1, cols do
io.write(string.format(“%d “, matrix[i][j])) — 空白区切りで出力
end
print() — 行末で改行
end
“`

table.concattable.sort

“`lua
print(“\n— table.concat —“)
local words = {“Lua”, “is”, “fun”}
print(table.concat(words, ” “)) — 出力: Lua is fun
print(table.concat(words, “-“)) — 出力: Lua-is-fun
print(table.concat(words, “”, 1, 2)) — 出力: Luais

print(“\n— table.sort —“)
local numbers = {9, 1, 5, 3, 7}
table.sort(numbers) — 昇順ソート
for _, v in ipairs(numbers) do io.write(v, ” “) end — 出力: 1 3 5 7 9
print()

local names = {“banana”, “apple”, “cherry”}
table.sort(names) — アルファベット順ソート
for _, v in ipairs(names) do io.write(v, ” “) end — 出力: apple banana cherry
print()

local mixedTypes = {3, “a”, 1, “b”}
— table.sort(mixedTypes) — エラーになる可能性がある (数値と文字列の比較)

— 比較関数を使ったソート (例: 文字列長でソート)
local fruits = {“apple”, “banana”, “cherry”, “date”}
table.sort(fruits, function(a, b)
return #a < #b — 文字列長の短い順
end)
for _, v in ipairs(fruits) do io.write(v, ” “) end — 出力: date apple banana cherry (順序は安定ソートによる)
print()
“`


これで、Luaの配列に関する詳細な解説とサンプルコードを含む記事は完成です。約5000語の要件を満たすよう、各セクションを丁寧に記述し、コード例も豊富に含めました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール