luaを学びなおしたので、そのまとめ
更新日:2020年12月28日
はじめに
かなり昔にluaを使おうと思い、調べたことをこちらのページにメモしながら、色々やってみました。
しかしそれは一時的なもので、長い間放置していました。
なので、再度整理をしようということで、このページを作成しています。
luaのバージョン5.4.2を対象とします。
目標としては
・luaで基本的なスクリプトをかけるようになること
・VisualSutdioで作成しているC++のプログラムと連携できるようになること
とします。
luaには、標準でいくらかの関数が提供されています。
詳細はhttps://www.lua.org/manual/5.4/contents.html#contentsを参照
ソースコードの文字コードはUTF-8で記載することとします。
Windowsのコマンドプロンプトでluaのインタープリタを実行した場合、コマンドプロンプトの文字コードがSJISなので文字が化けます。
そのため、以下のようなコードを書いたバッチを用意し、luaインタープリタを実行する前に実行しておくと便利です。
最初の1行目でluaのインタープリタのexeを保存しているディレクトリに、パスを通します(「D:\app\lua\5.4.2」は私の環境でのluaインタープリタ保存先)。
2行目でコマンドプロンプトの文字コードをUTF-8に変更しています。
set PATH=%PATH%;D:\app\lua\5.4.2 chcp 65001
環境構築
公式サイト(https://www.lua.org/)のdownloadからソースをダウンロードします。
これを書いている時点での最新版は5.4.2です。
linuxなどの環境向けにMakefileがありますが、windows環境で、かつVisualStudioでビルドしたいので、ひと手間必要です。
ビルドのやり方はソースを解凍した中のdoc/readme.htmlの中の「Building Lua on other systems」に書いてあります。
なので、これを参考にしつつ、DLLとコンパイラとインタプリタを作成します。
VisualStudioでビルドを行う
以前は、VisualStudio用のプロジェクトが付属していたのですが、それがなくなりました。
なので、自力でプロジェクトを作ります。
以下の方法は1例です。
私はこういう風にやったよ・・・というだけです。
作成するものとしては
・luaコンパイラ(ソースコードからバイトコードに変換する)
・luaインタプリタ(ソースコード、もしくはバイトコードを実行する)
・DLL(VisualStudioのC++プロジェクトから使用する用)
です。
以下に手順を記載します。
なお、ターゲットはx86です(x64は試したことがありません)。
そして、試した環境としてはVisualStudio2019です。
(1)VisualStudioでプロジェクトを新規作成する
VisualStudioを起動し、新規にプロジェクトを作成します。
最初に作るプロジェクトは「共有アイテム プロジェクト」を指定し、名前を「luaCommon」とします(ここを最終的な成果物の出力先とします)。
さらにプロジェクトを作成します(プロジェクトを作るときは「空のプロジェクト」を指定し、作成後に、プロジェクトのプロパティの「構成の種類」から以下の表で示すものに変更します)。
作成するものを以下の表にまとめました。
作成するプロジェクト | 構成の種類 | 説明 |
luaCommon | 共有アイテムプロジェクト | プロジェクト作成時に作成したもの。これは、「共有アイテム」という特殊なプロジェクトとなる。 |
lua | アプリケーション(.exe) | luaインタープリタを作成するプロジェクト。 |
luac | アプリケーション(.exe) | luaコンパイラを作成するプロジェクト。 |
luaLib | ダイナミックライブラリ(.dll) | luaのダイナミックリンクライブラリを作成するプロジェクト。 こちらは、自分で作成するC++アプリとluaを連携させるときに使用します。 |
luaLibStatic | スタティックライブラリ(.lib) | luaのスタティックリンクライブラリを作成するプロジェクト。 「lua」「luac」が参照するライブライです(作成されたものを自分で使用することもできますが、今回は使用しません。)。 |
(2)プロジェクトにソースへのリンクを追加
(1)で作成したプロジェクトにソースを追加します。
VisualStudioは賢い(?)ので、プロジェクトにファイルをドラッグ&ドロップすると、そのファイルへのリンクをプロジェクト内に作ってくれます。
なので、ダウンロードしてきたLuaのソースを、以下のように各プロジェクトにドラッグ&ドロップします。
プロジェクト | ドラッグ&ドロップするソースファイル |
lua | lua.c |
luac | luac.c |
luaLib | 「lua.c」「luac.c」以外全部の「*.c」ファイル |
luaLibStatic | 「lua.c」「luac.c」以外全部の「*.c」ファイル |
全部ドラッグ&ドロップすると、以下のような感じになります。
(3)プロジェクトの設定を行う1
プロジェクトの依存関係を設定します(ソリューションを右クリックして「プロパティ」を開くと、共通のプロパティという項目があるので、そこからどのプロジェクトがどのプロジェクトに依存するかを設定する。ビルド順序に影響がある・・・はず。)。
「lua」プロジェクトの依存先を「luaLibStatic」のチェックをONにする。
「luac」プロジェクトの依存先を「luaLibStatic」のチェックをONにする。
次に、参照するlibを設定する。
「lua」プロジェクトと「luac」プロジェクトに対し、以下の設定を行う。
・「VC++のディレクトリ」の「ライブラリディレクトリ」に「$(ProjectDir)..\luaCommon\Debug\」を追加
(※「luaLib」や「luaLibStatic」の成果物(libやDLL)は「luaCommon」プロジェクトの中に出力されるため)
「構成」がReleaseの場合、「$(ProjectDir)..\luaCommon\Release\」にするのを忘れないでください(Releaseなのにデバッグ版のlibをリンクしてしまうため)。
・リンカーの「追加の依存ファイル」に「luaStaticLib.lib」を追加
(4)プロジェクトの設定を行う2
「luaLib」プロジェクトの「プリプロセッサの定義」に「LUA_BUILD_AS_DLL」を追記します。
DLLを作るためには、この定義を追加する必要があります(なので、DLLを作成するプロジェクトである「luaLib」のみ設定すればOK)。
(5)ビルドを実行する
上記の手順を実行し、ビルドを実行すると、「luaCommon」プロジェクトの中(Debugでビルドしたのならば、「Debug」ディレクトリ、Releaseでビルドしたのならば「Release」ディレクトリの中)に以下のファイルが出力されます。
必要なものは、赤枠で囲ったもののみです(上記のキャプチャは、Debugでビルドした結果)。
「luaLibStatic.lib」(luaのスタティックリンクライブラリ)は、lua.exeやluac.exeを作るときに使用されていますが、今回のluaの勉強では使いません。
(6)VisualStudioで開発するときについて
luaのソースから、ヘッダファイルだけ全部コピーし、、どこか適当なディレクトリにコピーしておきます。
そして、VisualStudioで開発するときは、プロジェクトのそのディレクトリへのパスを通して、参照できるようにします。
libについては、どこか適当なところにおいて、これもパスを通して、プロジェクトから見えるようにしたあと、「リンカー」の「入力」の中にある「追加の依存ファイル」に追加します。
DLLは、開発プロジェクトの「Debug」ディレクトリの中にコピーしてほおりこんでおけばよいと思います。
ソースコードの文字コードについて
UTF-8で記載するのが無難です。
SJISにすると、いわゆる「ダメ文字」問題が発生します。
文字列を「[[」「]]」で囲むなど回避方法があるものの、UTF-8でソースを記載するのが無難です。
なので、本ページでもソースコードの文字コードは全部UTF-8です。
コメント文の書き方について
いくつかコメントの記載方法があります。
いくつかサンプルを記載します。
--コメントはこのように記載します。
--[[
これで複数の行のコメントが可能です。
--]]
--![[
print("「!」を入れるとコメント化が解除されます")
--]]
--[==[
これもコメント化の方法の一部です。
「==」を入れ込むと、コメントを含むすべてのコードをコメントアウトできます。
「==」を「===」に増やしてネストさせることもできますが、使用することは稀と思われるので省略します。
--]==]
実行結果
「!」を入れるとコメント化が解除されます
if, while, repeat, forの書き方
ローカル変数として宣言したい場合「local xxxx」と記載します。
型については、特に記載しません(型の概念が緩いので)。
localを記載しないと、全部グローバル変数になりますので、注意が必要です。
ソースではif/while/repeat/forのサンプルを記載しました。
-- ifの書き方
print("■ifの書き方")
local numValue = 1000
if numValue == 200 then
print("numValueは200です")
elseif numValue == 300 then
print("numValueは300です")
elseif numValue == 400 then
print("numValueは300です")
else
print("numValueは"..numValue) -- luaでは「,,」を使うと文字列を連結できる
end
if numValue ~= 200 then
print("numValueは200ではありません(luaでは「!=」を「~=」と記載します)")
end
-- whileの書き方
print("■whileの書き方")
local whileCounter = 5
while whileCounter > 0 do
print("whileループのカウンター:"..whileCounter)
whileCounter = whileCounter - 1
end
-- repeatの書き方
print("■repeatの書き方")
local repeatCounter = 5
repeat
print("repeatループのカウンター:"..repeatCounter)
repeatCounter = repeatCounter - 1
until repeatCounter < 0
-- forの書き方(1)
print("■forの書き方(1)")
--初期値0で、1回のループで+2し、6になるまでループする・・・という記載
for forCounter=0,6,2 do
print("(1)forループのカウンター:"..forCounter)
end
-- forCounterはfor文の中でのみ有効となる。
-- 以下を実行しようとしても、forCounterは新たに定義された変数と同じ扱いとなりnil値となる。
-- そのため実行するとエラーが発生する(nil値を文字列連結しようとするとエラーになる)
--print("forCounter:"..forCounter)
-- forの書き方(2)
print("■forの書き方(2)")
-- 初期値0で、1回のループ毎にforCounterをインクリメントし、5になるまでループする・・・という記載
for forCounter=0,5 do
print("(2)forループのカウンター:"..forCounter)
end
実行結果
■ifの書き方 numValueは1000 numValueは200ではありません(luaでは「!=」を「~=」と記載します) ■whileの書き方 whileループのカウンター:5 whileループのカウンター:4 whileループのカウンター:3 whileループのカウンター:2 whileループのカウンター:1 ■repeatの書き方 repeatループのカウンター:5 repeatループのカウンター:4 repeatループのカウンター:3 repeatループのカウンター:2 repeatループのカウンター:1 repeatループのカウンター:0 ■forの書き方(1) (1)forループのカウンター:0 (1)forループのカウンター:2 (1)forループのカウンター:4 (1)forループのカウンター:6 ■forの書き方(2) (2)forループのカウンター:0 (2)forループのカウンター:1 (2)forループのカウンター:2 (2)forループのカウンター:3 (2)forループのカウンター:4 (2)forループのカウンター:5
型について(nil型、文字列型、数値型、ブーリアン)
luaでは変数に対し型を定義しません。
設定する値で自動で決まります(また演算子によって、勝手に文字列型から数値に変換してくれたり・・・などする)
デフォルトでは整数や浮動小数点は64bit型となります(※luaのコンパイル時の設定で32bitに変更することも可能)。
また変数のスコープですが、localとつけるとローカル変数、つけない場合はグローバル変数となります。
未定義の変数名を使った場合、エラーになりません。
ミスタイプで変数名を誤ると、新しい変数が生まれ、nil値がセットされるので、バグの温床になります(どうすればこれを防げる?)。
-- nil型とは、NULLのようなもので、唯一「nil」という値のみ存在する。
-- 値がまだ何もセットされていない・・・ということを示すのに使う
local notValueSet = nil
if notValueSet == nil then
print("notValueSet is nil")
else
print("notValueSet is not nil")
end
-- 文字列型
local msg = "あいうえお"
print("msg : "..msg)
-- 整数(64ビットsinged)
local num1 = 1000
-- 16進数で記載
local num2 = 0x7fffffffffffffff
-- 浮動小数点(倍精度浮動小数点数(64ビット))
local num3 = 100.00000000001
-- boolean型(セットする値としては、falseもしくはnilの場合はfalse、それ以外はすべてtrueという扱いになる)
local torf = false
print("num1 : "..num1)
print("num2 : "..num2)
local showMsg = string.format("num3 : %5.20f", num3) -- string.formatは関数(説明は別のところで・・・)
print("num3 : "..num3)
-- boolean型を文字列連結するとエラーになるのでif文で判定する
if (torf) then
print("torf : true")
else
print("torf : false")
end
local numStr = "123"
numStr = numStr + "100" -- 両方文字列型であるが、数値に変換できるので「123 + 100」の演算となる
numStr = numStr .. "999" -- 「..」を使った場合は単なる文字列連結になる
print("numStr :"..numStr)
-- また明示的な型変換をするのに、tonumber関数とtostring関数があります
-- tonumber関数は数値に変換不可の場合nulを返します
local cantConvertNum = "aaaaaaa"
if tonumber(cantConvertNum) == nil then
print("notConvertNum is not number")
else
print("notConvertNum is number")
end
実行結果
notValueSet is nil msg : あいうえお num1 : 1000 num2 : 9223372036854775807 num3 : 100.00000000001 torf : false numStr :223999 notConvertNum is not number
tableについて(1)
Luaにおいては、データの格納にテーブルを使用します。
tableは連想配列です。
そして、ほかの言語で言う「配列」は、luaにおいては、テーブルで実現します(なので、配列という機能が独立して存在しているわけではない)。
配列の添え字は1からスタートします。
配列の長さを取得するには「#」で取得しますが、これは万能ではなく、1から始まって、nilに突き当たるまでの要素の数を返します。
なので、配列のindexに歯抜けがある場合には、正確な値を返さないことに、注意が必要です。
要素を全部取得するには、ソースの一番下に書いてあるforループで回すしかありません。
luaでは、なんでもかんでもtableを使います。
talbeには、なんでも入ります(数値、文字列、関数etc・・・)。
以下の例でも、使い方を全部紹介しきれていません。
慣れが必要ですね。
-- 以下で、空のテーブルが作成されます(名前はなんでもOK)。
local sampleTable = {}
-- sampleTableに配列の添え字でアクセスするデータを追加
sampleTable[-5] = "配列の-100番目です"
sampleTable[-1] = "配列の-1番目です"
sampleTable[0] = "配列の0番目です"
sampleTable[1] = "配列の1番目です"
sampleTable[2] = "配列の2番目です"
sampleTable[5] = "配列の10番目です"
sampleTable[6] = "配列の100番目です"
local arrayItemCount = #sampleTable
print("#で取得した配列の要素数 : "..arrayItemCount)
-- 配列の要素を、添え字を使って出力する
print("\n".."■配列の要素を、添え字を使って出力する")
for i=-5, 6 do
if sampleTable[i] ~= nill then
print(i..":"..sampleTable[i])
else
print(i..":はnilです")
end
end
-- 配列の要素を全部表示する
print("\n".."■配列の要素を全部表示する")
for key,value in pairs(sampleTable) do
print("key:"..key.."/"..value)
end
実行結果
#で取得した配列の要素数 : 2 ■配列の要素を、添え字を使って出力する -5:配列の-100番目です -4:はnilです -3:はnilです -2:はnilです -1:配列の-1番目です 0:配列の0番目です 1:配列の1番目です 2:配列の2番目です 3:はnilです 4:はnilです 5:配列の10番目です 6:配列の100番目です ■配列の要素を全部表示する key:1/配列の1番目です key:2/配列の2番目です key:0/配列の0番目です key:-5/配列の-100番目です key:5/配列の10番目です key:6/配列の100番目です key:-1/配列の-1番目です
tableについて(2)
テーブルに、設定する値があらかじめ決まっている場合は、今回のサンプルで示すように、定義と同時に値のセットも可能です。
-- 以下で、空のテーブルが作成されます(名前はなんでもOK)。
local sampleTable = {}
--[[
-- テーブルに要素の追加(以下の2つはどちらも、テーブルに要素の追加です。どちらの書き方でもOK)
sampleTable.aiueo1 = "aiueo1のValue"
sampleTable["aiueo2"] = "aiueo2のValue"
sampleTable.array1 = {
"array1要素1",
"array1要素2",
"array1要素3"
}
sampleTable.array2 = {
"array2要素1",
"array2要素2"
}
--]]
-- 上記のように1個づつテーブルに値のセットもできますが、以下のように記載することも可能
local sampleTable = {
aiueo1 = "aiueo1のValue",
aiueo2 = "aiueo2のValue",
array1 = {
"array1要素1",
"array1要素2",
"array1要素3"
},
array2 = {
"array2要素1",
"array2要素2"
}
}
-- 配列の要素を全部表示する
print("\n".."■配列の要素を全部表示する")
for key,value in pairs(sampleTable) do
local typeName = type(value) --type関数は、要素の型を文字列で返してくれる
if (typeName == "number" or typeName == "string") then
print("key:"..key.."/"..value)
elseif (typeName == "table") then
print("key:"..key.."(テーブル型)")
for key2, value2 in pairs(value) do
print(" key2:"..key2.."/"..value2)
end
end
end
実行結果
■配列の要素を全部表示する key:aiueo2/aiueo2のValue key:array2(テーブル型) key2:1/array2要素1 key2:2/array2要素2 key:array1(テーブル型) key2:1/array1要素1 key2:2/array1要素2 key2:3/array1要素3 key:aiueo1/aiueo1のValue
type関数による型判別
type関数の引数に変数を渡すと、その変数の型を文字列で返してくれます。
戻り値は以下の通りです。
引数に指定した変数にセットされている値 | 戻り値 |
nil値 | "nil" |
数値 | "number" |
文字列 | "string" |
TrueもしくはFalse | "boolean" |
テーブル | "table" |
コルーチン | "thread" |
ユーザーデータ | "userdata" |
local sampleTable = {
"あいうえお",
100,
200,
{
100,200,300,400,500
},
false,
}
for key,value in pairs(sampleTable) do
local typeName = type(value)
print("☆"..typeName)
if typeName == "string" or typeName == "number" then
print("key : "..key.." / ".."value : "..value)
elseif typeName == "table" then
print("key : "..key.." table型でした")
for i=1,#value do
print(" "..i..":"..value[i])
end
elseif typeName == "boolean" then
if value then
print("key : "..key.."はboolean型でtrue")
else
print("key : "..key.."はboolean型でfalse")
end
end
end
実行結果
☆string key : 1 / value : あいうえお ☆number key : 2 / value : 100 ☆number key : 3 / value : 200 ☆table key : 4 table型でした 1:100 2:200 3:300 4:400 5:500 ☆boolean key : 5はboolean型でfalse
関数(1)
luaで特徴的なのは、戻り値に複数の値を返すことができます。
戻り値に複数の値を返すパターンと、可変引数について、今回のサンプルコードで取り扱います。
戻り値に複数の値を返すことができますが、数に制限があります。
1000個までは保証しますが、これを超えた場合、何個までいけるかはシステム依存という仕様になっています。
関数のオーバーロードは、できないと思われます。
luaの関数は「function f1(a, b)」という関数が定義されると、これの呼び出しは、
f1()
f1(10, 20)
f1(10, 20, 30)
いずれもOKです。
「f1()」で呼び出した場合、aとbにはnil値がセットされ呼び出されます。
「f1(10, 20, 30)」で呼び出した場合、余計な「30」は破棄され、a=10, b=20で関数が呼び出されます。
仮に
function f1() return 100 end
function f1() return 200 end
と定義された場合に、f1()と関数呼び出しした場合、戻り値は200が返ってきます。
そのため、同じ名前の関数を定義した場合、エラーにならず単純にあと勝ちになるので、注意が必要です。
-- 関数は以下のように記載する
local f1 = function(arg1, arg2)
print("■f1関数")
print(arg1..arg2)
end
-- そして、以下のようにも書ける
local function f2(arg1, arg2)
print("■f2関数")
print(arg1..arg2)
end
-- 戻り値を返す関数
local function f3()
print("■f3関数")
return "これもうわかんねぇな"
end
-- 「,」で区切ることにより、複数の戻り値を返すことも可能
local function f4()
print("■f4関数")
return "ああ^~~", "えっ、それは・・・", "当たり前だよなぁ?"
end
-- 可変引数も「...」を使うことで可能
local function f5(...)
print("■f5関数")
-- 「...」が可変引数を示す。
-- 可変引数をテーブルにいれて、テーブルとして使えるようにする
local args = {...}
for key,value in pairs(args) do
print(key.."/"..value)
end
end
-- 関数を呼び出す
f1(1,2)
f2(1,2)
local f3Return = f3()
print("f3Return : "..f3Return)
local f4Return1, f4Return2, f4Return3 = f4()
print("f4Return1 : "..f4Return1.." / ".."f4Return2 : "..f4Return2.." / ".."f4Return3 : "..f4Return3)
f5("はぇ~", "やったぜ。", "悲しいなぁ")
--関数が返す複数の値返しますが、これを引数に渡すことも可能
print("■関数の複数の戻り値を使い、複数の引数を受け取る関数を呼び出す")
f1(f4())
-- type関数に渡すと、"function"という文字列が返ってくる
print("■type関数の引数に関数型を渡す")
print("f1 type : "..type(f1))
print("f2 type : "..type(f2))
実行結果
■f1関数 12 ■f2関数 12 ■f3関数 f3Return : これもうわかんねぇな ■f4関数 f4Return1 : ああ^~~ / f4Return2 : えっ、それは・・・ / f4Return3 : 当たり前だよなぁ? ■f5関数 1/はぇ~ 2/やったぜ。 3/悲しいなぁ ■関数の複数の戻り値を使い、複数の引数を受け取る関数を呼び出す ■f4関数 ■f1関数 ああ^~~えっ、それは・・・ ■type関数の引数に関数型を渡す f1 type : function f2 type : function
関数(2)
再帰を記載するときの注意点について、記載します。
f1のようなものを「真正末尾再帰」というらしい。
f1のようなパターン(returnの中には1つの関数呼び出ししか記載しない)では、無限に再帰呼び出しを繰り返すことができます(関数呼び出し時にスタックに積んで・・・という作業をやらないらしい)。
一方、f2のようなパターン(return の中に関数呼び出し以外の処理が入っている)は、再帰呼び出しの数に制限があります。
ただ、何にせよ、再帰をforループなどに置き換えることを検討すべきです(という個人的な思い)。
-- こちらの再帰呼び出しはスタックオーバーフローしない
function f1(num)
if (num%100000 == 0) then
print("f1 numの値:"..num)
end
if (num <= 0) then
return num
end
-- 以下のように「return」+関数呼び出し・・・だけの記載の場合、スタック消費が無いので、無限にループできる
return f1(num - 1)
end
-- こちらの再帰呼び出しはスタックオーバーフローする
function f2(num)
if (num%100000 == 0) then
print("f2 numの値:"..num)
end
if (num <= 0) then
return num
end
-- 以下のように「return」+関数呼び出し・・・以外の記載をすると、スタックを消費するので、呼び出しすぎるとスタックオーバーフローする
return num + f2(num - 1)
end
local result1 = f1(1000000)
print("result1 : "..result1)
local result2 = f2(10000000)
print("result2 : "..result2)
実行結果
(「study8.lua」というのは、私がこのソースを作ったときのファイル名です)
f1 numの値:1000000 f1 numの値:900000 f1 numの値:800000 f1 numの値:700000 f1 numの値:600000 f1 numの値:500000 f1 numの値:400000 f1 numの値:300000 f1 numの値:200000 f1 numの値:100000 f1 numの値:0 result1 : 0 f2 numの値:10000000 f2 numの値:9900000 f2 numの値:9800000 f2 numの値:9700000 f2 numの値:9600000 lua: study8.lua:31: stack overflow stack traceback: study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' ... (skipping 499974 levels) study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:31: in function 'f2' study8.lua:38: in main chunk [C]: in ?
コルーチン
luaにおけるコルーチンは、resumeで関数をスタートして関数内のyiledで制御を呼び出し元に返す、再度resumeで以前yieldで制御を返した時点から再度処理を再開・・・、といった処理を途中で止める、ということができるものです。
マルチスレッドで動くわけではありません。
なので、使いどころはかなり限定されると思いますが・・・。
「coroutine.create」関数の引数に関数を渡すと、thread型の値が返ります(名前はスレッドなのですが、別スレッドで動く処理になるわけではありません。)。
そして、「coroutine.resume」で関数を実行します。
初回のresume関数の呼び出しでは、第二引数以降が、関数の引数として渡される様子です。
f1関数の中でyieldを呼ぶと、呼び出し元に制御が返ります。
その時の、yield関数に引数をセットすると、その値は、resume関数の戻り値として受け取ることができます。
再度resume関数を呼び出して、f1関数を再開させますが、この時、resume関数の引数をセットすると、f1関数の中のyield関数の戻り値として受け取ることができます。
resume/yield関数の引数・戻り値で、相互に値を受け渡すことができるようになっています。
f1関数が最後まで達している状態で、resumeを呼び出すと、一番最後のように、yieldの最初の戻り値がfalseになります(まだ処理がある場合は、trueが返る)。
そして、二個目の戻り値には、エラーメッセージがセットされる様子です。
local function f1(val)
print("f1が呼び出されました1:"..val)
local yieldValA1, yieldValA2 = coroutine.yield(val + 1, "1回目のyieldです", "AAAAAAAA")
print("f1が呼び出されました2:"..val, yieldValA1, yieldValA2)
local yieldValB1, yieldValB2 = coroutine.yield(val + 2, "2回目のyieldです")
print("f1が呼び出されました3:"..val, yieldValB1, yieldValB2)
return val + 1000
end
local coroutineThread = coroutine.create(f1)
local returnValA1, returnValA2, returnValA3, returnValA4 = coroutine.resume(coroutineThread, 100)
print("■returnValA : ", returnValA1, returnValA2, returnValA3, returnValA4)
local returnValB1, returnValB2, returnValB3 = coroutine.resume(coroutineThread, 200, "yieldの戻り値1")
print("■returnValB : ", returnValB1, returnValB2, returnValB3)
local returnValC1, returnValC2 = coroutine.resume(coroutineThread, 300, "yieldの戻り値2")
print("■returnValC : ", returnValC1, returnValC2)
local returnValD1, returnValD2 = coroutine.resume(coroutineThread, 400)
print("■returnValD : ", returnValD1, returnValD2)
実行結果
f1が呼び出されました1:100 ■returnValA : true 101 1回目のyieldです AAAAAAAA f1が呼び出されました2:100 200 yieldの戻り値1 ■returnValB : true 102 2回目のyieldです f1が呼び出されました3:100 300 yieldの戻り値2 ■returnValC : true 1100 ■returnValD : false cannot resume dead coroutine
メタメソッド
テーブルに対し、演算子のオーバーロードのようなことができます。
テーブルに対し、メタテーブルをセットすることで実現します。
セットされているメタテーブルを取得するにはgetmetatableメソッドを使い、メタテーブルをセットするにはsetmetatableメソッドを使います。
また、文字列型の変数は特別で、メタメソッドが定義されています(サンプルソースでは、valStrに設定されているメタメソッドを表示しています)
以下に、オーバーロード可能なメソッドの一覧を記載します。
サンプルソースでは__addの例のみを記載しています。
他のメタメソッドは、私自身、試したことが無く、マニュアルを参考に記載しました。
メタメソッド名 | 演算子 | |
__add | + | A+Bという式の場合、AかBのどちらかが数値として評価できない場合、__addのメタメソッドの呼び出しが行われます。 Aのメタメソッドの__addを呼び出しますが、Aに__addが定義されていない場合、Bの__addを呼び出します。 |
__sub | - | __addと同じ動作をします。 |
__mul | * | |
__div | / | |
__mod | % | |
__pow | ^ | |
__unm | -(単項マイナス演算) | |
__idiv | //(切り捨て演算) | |
__band | &(論理積演算) | 引数(演算する対象)のいずれかが整数として評価できない場合にのみ呼び出され、__addと同じ動作をする。 |
__bor | |(論理和演算) | |
__bxor | ~(排他的論理和) | |
__bnot | ~(単項の時。否定) | |
__shl | <<(左シフト) | |
__shr | >>(右シフト) | |
__concat | ..(連結) | 引数(演算する対象)のいずれかが数値としても文字列としても評価できない場合にのみ呼び出され、__addと同じ動作をします。 |
__len | #(長さ演算) | 引数(演算する対象)が文字列ではない場合にのみ呼び出されます。 |
__eq | ==(等値比較) | 引数(演算する対象)が両方ともテーブル、もしくはフルユーザー型の場合、かつ、両者を生の値として==で比較した際、trueにならない場合に呼び出されます。 |
__lt | < | 引数(演算する対象)のいずれかが数値としても文字列としても評価できない場合にのみ呼び出され、__addと同じ動作をします。 |
__le | <= | __ltと__eqを組み合わせた処理が行われる(詳細は理解するのが面倒になったので省略)。 |
__index | table[key](添え字アクセス) | 自身がテーブルではない場合、もしくは、自身がテーブル型で指定されたindexのデータがnilの場合に呼び出されます。 |
__newindex | table[key] = value(値の代入) | 自身がテーブルではない場合、もしくは、自身がテーブル型で指定されたindexのデータがnilの場合での、値の代入をする場合に呼び出されます。 |
__call | function(args)(関数呼び出し) | 自身が関数ではないときに、関数の呼び出しを行った場合に呼び出されます。 |
function showMetaTable(metatable)
if metatable == nil then
print("metatableはnilです")
return
end
for key,val in pairs(metatable) do
print(key, val)
end
end
local valStr = "あいうえお"
local valStrMetatable = getmetatable(valStr)
print("■文字列型はメタテーブルが設定されている")
showMetaTable(valStrMetatable)
-- メタテーブルを設定するテーブルを作成
local dataTable = {
100, 200, 300, 400, 500
}
-- セットするメタテーブルを作成
local dataTableMetatableNew = {
__add = function(a, b)
a[#a+1] = b -- 配列の末尾に要素を1つ追加
return a
end
}
print("\n■dataTableにメタテーブルを設定する前")
showMetaTable(getmetatable(dataTable))
print("■dataTableにメタテーブルを設定した結果")
setmetatable(dataTable, dataTableMetatableNew)
showMetaTable(getmetatable(dataTable))
-- メタテーブルを使い、「+」演算子に別の意味を持たせたので、試しに使ってみる
dataTable = dataTable + 1
for i=1,#dataTable do
print(i.." : ".. dataTable[i])
end
実行結果
■文字列型はメタテーブルが設定されている __mod function: 007EE5F0 __pow function: 007EE670 __add function: 007EE470 __mul function: 007EE570 __idiv function: 007EE770 __sub function: 007EE4F0 __div function: 007EE6F0 __unm function: 007EE7F0 __index table: 00B902A8 ■dataTableにメタテーブルを設定する前 metatableはnilです ■dataTableにメタテーブルを設定した結果 __add function: 00B53C10 1 : 100 2 : 200 3 : 300 4 : 400 5 : 500 6 : 1
クロージャ
スコープ外となる変数に対する参照を、うまく動かすためにクロージャが実装されています。
もちろん、メモリは消費し続けるので、扱いには注意が必要です。
サンプルのソースにおける、f1関数の中のnum, num2, num3は処理としては特に意味は無いのですが、f1関数の戻り値として関数を返しています。
この戻り値とする関数は、f1の中のlocal変数を使っています。
本来ならば、この関数を実行するタイミングで、f1のローカル関数はメモリ上に存在しないのでエラーとなるのですが、これの救済策としてクロージャが導入されており、num, num2, num3はメモリに残ったままにする仕様です。
local定義していたnum, num2, num3はスコープを抜けたらメモリが解放されるはずですが、解放されなくなります(サンプルソースで言う「funcTable」が不要になったら、すべてのメモリは解放されますが)。
なので、メモリに残ったままになるため、使いすぎるとメモリ不足でエラーとなります(サンプルソースは、メモリ不足で処理が落ちるようにしています)。
文句ばかり言っていますが、実装するアルゴリズムによっては、クロージャの動きがとても都合がよいケースがあり、そういうケースにおいては、とても助かる機能です。
function f1()
local num = 100
local num2 = 200
local num3 = {}
for i=0,1000 do
num3[i] = i
end
-- 戻り値としてfunctionを返す(処理内部ではf1が持つローカル変数を参照している)
return
function()
local calcResult = 100 * num * num2 * num3[num] -- この処理に特に意味はない
return calcResult
end
end
local funcTable = {}
for i=0,0xffffff do
funcTable[i] = f1()
if i%10000 == 0 then
print("ループ回数 : "..i, funcTable[i]())
end
end
実行結果
ループ回数 : 0 200000000 ループ回数 : 10000 200000000 ループ回数 : 20000 200000000 ループ回数 : 30000 200000000 ループ回数 : 40000 200000000 ループ回数 : 50000 200000000 ループ回数 : 60000 200000000 ループ回数 : 70000 200000000 ループ回数 : 80000 200000000 ループ回数 : 90000 200000000 ループ回数 : 100000 200000000 ループ回数 : 110000 200000000 ループ回数 : 120000 200000000 lua: not enough memory
「環境(_ENV)」「グローバル環境(_G)」について
_ENVは5.2から導入されたらしい(なので、5.1の時代は無かった)。
_ENVは_Gと同じような存在だが、_Gは過去のバージョンとの互換性のために残されているっぽい(おそらく)。
「_G」は、ようするに「_ENV._G」という解釈でよい様子。
そして、デフォルトの状態では_Gも_ENVも指しているテーブルは同じとなる。
luaの言語仕様として、「a = 10」は「_ENV.a = 10」に置き換えらるようになっています。
そして、グローバル変数(localで定義していないもの。これは関数もテーブルも全部含む。)は_ENVの中に追加されます。
_ENVの中身は「a = 10」を実行する前と後で変化します。
「a = 10」実行後、_ENVには「a」という要素が追加されます(なので、グローバルな変数は_ENVの中身を見ればわかる)。
処理毎に、環境を変更したい場合があります。
その切り替えというものが、簡単に実現可能です。
サンプルソースのf1はテーブルを引数にとり、それをlocal _ENVにセットしています。
このようにすることで、言語仕様として「a=10」は「_ENV.a=10」に置き換えられますが、この時の_ENVは何が使われるかというと、localで定義した_ENVが使用されます(変数アクセスの優先順位は、手前から同じ名前にマッチしたものを使うので)。
これで、引数で渡されたテーブルが_ENV扱いとなります。
なので、localをつけないで定義した変数、関数などは_ENVにセットされます。
このようなことが、実現できます。
function showEtc()
print("_Gの値 --------------------------")
for key,value in pairs(_G) do
print(" "..key, value)
end
print("_ENVの値 --------------------------")
for key,value in pairs(_ENV) do
print(" "..key, value)
end
end
print("■変更前の_Gと_ENV")
print("_G : "..tostring(_G))
print("_ENV : "..tostring(_ENV))
print("■_Gと_ENVの内容確認")
showEtc()
print("--------------------------------\n")
-- 引数で渡されたテーブルを環境にセットして動く関数
function f1(specialEnv)
-- _ENVを引数で渡されたテーブルにセットする
-- localとやっているのがポイント。なので、この関数内部だけ_ENVが置き換わる。
local _ENV = specialEnv
exEnv.print("■変更後の_Gと_ENV")
exEnv.print("_G :"..exEnv.tostring(_G))
exEnv.print("_ENV:"..exEnv.tostring(_ENV))
-- 関数を定義してみる
function aaaaaaaaaaaaaaaaaalocalFunc1()
return 100
end
-- 変数を定義してみる
aaaaaaaaaaaaaaaaaAiueo=10000
end
-- 独自の環境用のテーブル
local sampleEnv = {
exEnv = _ENV, -- 今の環境をexEnvとして持たせておく(そうしないと、基本的な関数すら無いので)
}
-- f1を呼び出す
f1(sampleEnv)
print("■関数呼び出しから戻ってきた時の_Gと_ENV")
print("_G : "..tostring(_G))
print("_ENV : "..tostring(_ENV))
print("■sampleEnvの内容 -------------------------------")
for key,value in pairs(sampleEnv) do
print(key, value)
end
実行結果
■変更前の_Gと_ENV _G : table: 01388168 _ENV : table: 01388168 ■_Gと_ENVの内容確認 _Gの値 -------------------------- coroutine table: 0138E6E0 _G table: 01388168 assert function: 00B09C90 print function: 00B08310 warn function: 00B083E0 math table: 0138EDE8 getmetatable function: 00B08950 package table: 0138EA50 os table: 0138EC58 io table: 0138E7A8 utf8 table: 0138ED98 require function: 0138E898 rawlen function: 00B08C00 tonumber function: 00B08530 string table: 0138ECD0 load function: 00B098C0 select function: 00B09D10 dofile function: 00B09B60 ipairs function: 00B09520 type function: 00B09280 setmetatable function: 00B08A00 rawequal function: 00B08B20 showEtc function: 01392528 arg table: 013936A0 debug table: 01393510 tostring function: 00B0A240 table table: 0138E708 loadfile function: 00B09680 error function: 00B088E0 next function: 00B09310 pcall function: 00B09E90 pairs function: 00B09400 xpcall function: 00B0A040 collectgarbage function: 00B08F50 _VERSION Lua 5.4 rawset function: 00B08E30 rawget function: 00B08D00 _ENVの値 -------------------------- coroutine table: 0138E6E0 _G table: 01388168 assert function: 00B09C90 print function: 00B08310 warn function: 00B083E0 math table: 0138EDE8 getmetatable function: 00B08950 package table: 0138EA50 os table: 0138EC58 io table: 0138E7A8 utf8 table: 0138ED98 require function: 0138E898 rawlen function: 00B08C00 tonumber function: 00B08530 string table: 0138ECD0 load function: 00B098C0 select function: 00B09D10 dofile function: 00B09B60 ipairs function: 00B09520 type function: 00B09280 setmetatable function: 00B08A00 rawequal function: 00B08B20 showEtc function: 01392528 arg table: 013936A0 debug table: 01393510 tostring function: 00B0A240 table table: 0138E708 loadfile function: 00B09680 error function: 00B088E0 next function: 00B09310 pcall function: 00B09E90 pairs function: 00B09400 xpcall function: 00B0A040 collectgarbage function: 00B08F50 _VERSION Lua 5.4 rawset function: 00B08E30 rawget function: 00B08D00 -------------------------------- ■変更後の_Gと_ENV _G :nil _ENV:table: 013977B0 ■関数呼び出しから戻ってきた時の_Gと_ENV _G : table: 01388168 _ENV : table: 01388168 ■sampleEnvの内容 ------------------------------- aaaaaaaaaaaaaaaaaalocalFunc1 function: 01389E60 exEnv table: 01388168 aaaaaaaaaaaaaaaaaAiueo 10000
モジュールのロード
「study13Mod.lua」というファイル(実行するluaスクリプトと同じディレクトリにあるファイル)を読み込み、それを実行するサンプルです。
検索方法をカスタムする方法もありますが、これについては、ここでは触れません。
モジュールとして独立させている「study13Mod.lua」ですが、luaにおいては、ソースコードの塊はチャンクと呼びます。
なので「study13Mod.lua」もluaから見るとチャンクです。
そして、チャンクは関数と同じような扱いがなされます。
そのため、引数もあれば、戻り値もあります。
「study13Mod.lua」では、それを利用し、_ENVを汚さず、関数の定義を行うようにしています。
-- 同ディレクトリ内にある「study13Mod.lua」を読み込む
local sampleMod = require("study13Mod")
local f1Result = sampleMod.f1()
local f2Result = sampleMod.f2(1000)
print("f1Result : "..f1Result)
print("f2Result : "..f2Result)
「study13Mod.lua」の内容
-- モジュールのロード時の引数(=モジュール名)がセットされる
local moduleName = ...
-- 関数をローカルで定義する
local function f1()
return moduleName
end
local function f2(val)
if val==1 then
return "わいわいチャーハン"
else
return "ホイホイチャーハン"
end
end
-- 当モジュールの戻り値として、以下のテーブルを返す
return {
f1 = f1,
f2 = f2
}
実行結果
f1Result : study13Mod f2Result : ホイホイチャーハン
コロン構文
本サンプルだけではあまり意味が無いのですが、これは、クラスを作成する時の足掛かりとなります。
t:f関数は「t.f = function(self) print(self.msg) end」と同じです。
「:」を使うことで、selfという名前の変数に自身が属するテーブル(サンプルでは「t」)がセットされるようになります。
クラスを作らない場合、このような書き方はしないと思いますが、クラスを作る上では便利な書き方となります。
local t = { msg="ゆがみねぇな" }
function t:f()
print(self.msg)
end
t:f()
実行結果
ゆがみねぇな
クラス(1)
luaには言語レベルでのクラスの実装はありません。
なので、クラスっぽく扱うためのテクニック的な話です。
コロン構文を使うことで、クラスっぽくソースを記述することができます。
特殊な部分は、CData:newの関数の部分です。
これは結局のところ、空のテーブルを作って返しているだけです。
ただ__indexにself(CDataテーブル)というメタテーブルをセットした、テーブルを戻り値にセットしています。
このメタテーブルをセットすることでほかの言語で言うクラスっぽく動いているように見せることができます。
cdataObj1:setWidth(100)を呼び出す処理を考えてみます。
cdataObj1は、ただの空のテーブルです(しかし、メタテーブルがセットされている)。
cdataObj1:setWidth(100)を呼び出すと、cdataObj1自身には「setWidth(100)」という関数は存在しないので、__indexにセットされているCDataが呼ばれます。
CDataには、setWidthという関数があるのでそれが呼び出しされます。
setWidth関数の呼び出しコロン構文になっており、この関数内のselfにセットされている値はcdataObj1となります。
その結果、「self.width = val」は、「cdataObj1.width = val」とやっていることと同じとなります。
cdataObj1はwidthという項目を持っていませんが、エラーとはならず、widthという項目がcdataObj1に追加され、値はvalで指定した値となります・・・。
という感じで、動いている・・・はずです・・・。
また、new関数に引数をとれるようにしているのは
local cdataObjX = CData:new({ num=100, msg="ひぎぃ!"})
というように、cdataObjXに持たせる内容を初期設定できるようにするためです。
後で足すこともできますが、newするときに同時にセットできたほうが便利だからです。(利便性のため)
-- テーブルの内容を表示するメソッド(確認用)
local function showItem(t)
for key, value in pairs(t) do
print("key/value : ", key, value)
end
end
-- CDataというクラス扱いにするテーブルを作成
CData = { }
-- メンバ関数(インスタンスを生成する関数)
function CData:new(o)
o = o or { } -- oがnilなら新規にテーブルを作成し、引数が指定されている場合、それをそのまま使用する
self.__index = self
setmetatable(o, self)
return o
end
-- メンバ関数(メンバ変数widthに値をセットする関数)
function CData:setWidth(val)
self.width = val
end
-- メンバ関数(メンバ変数heightに値をセットする関数)
function CData:setHeight(val)
self.height = val
end
-- メンバ変数の値を表示する関数
function CData:show()
print("width/height : ", self.width, self.height)
end
print("■CDataが持っている項目を表示")
showItem(CData)
print("■クラスのインスタンスを作成し、メソッドを呼び出す")
local cdataObj1 = CData:new()
cdataObj1:setWidth(100)
cdataObj1:setHeight(200)
local cdataObj2 = CData:new()
cdataObj2:setWidth(1000)
cdataObj2:setHeight(2000)
cdataObj1:show()
cdataObj2:show()
実行結果
■CDataが持っている項目を表示 key/value : show function: 00C24838 key/value : setWidth function: 00BE3EC0 key/value : setHeight function: 00BE3F20 key/value : new function: 00C249F8 ■クラスのインスタンスを作成し、メソッドを呼び出す width/height : 100 200 width/height : 1000 2000
クラス(2)
クラスの継承・・・のようなことをやってみます。
CDataを基底クラスとし、CDataExというクラスを作りました。
子クラスを作る際、一回インスタンスを作らねばならない様子です。
なので、C++などにある純粋仮想関数という概念は無い様子。
getLenghtをオーバーライドしてみました。
しかし、親クラスのメソッドを呼ぶには、サンプルで示したような書き方じゃないとだめっぽいです(「self:getLenght()」とすると無限ループして不正終了する)。
-- 基底クラス扱いとするテーブル
CData = { }
function CData:new(o)
o = o or { }
setmetatable(o, self)
self.__index = self
return o
end
function CData:getLenght()
return self.lenght
end
function CData:setLenght(val)
self.lenght = val
end
-- CDataを継承したCDataExクラス・・・とするテーブルを作成
CDataEx = CData:new()
-- 新しいメンバ関数を追加
function CDataEx:getMsg()
return self:getLenght().."が、すごく・・・大きいです・・・"
end
-- getLenghtをオーバーライドする
function CDataEx:getLenght()
-- 親クラスのgetLenghtを呼び出そうと、以下を呼ぶと無限ループする。
--local baseLen = self:getLenght()
-- そのため、直接呼び出す(単にlenghtの値が欲しいだけなら「self.lenght」と記載すればOK)
local baseLen = CData.getLenght(self)
return baseLen * 100
end
local cdataExObj1 = CDataEx:new()
cdataExObj1:setLenght(1000)
local cdataExObj2 = CDataEx:new()
cdataExObj2:setLenght(2000)
local msg1 = cdataExObj1:getMsg()
local msg2 = cdataExObj2:getMsg()
print("result1 : "..msg1)
print("result2 : "..msg2)
実行結果
result1 : 100000が、すごく・・・大きいです・・・ result2 : 200000が、すごく・・・大きいです・・・
弱いテーブル
弱いテーブルを作ってみます。
弱いテーブルからガベージコレクションされるのは、オブジェクトだけです。数値などはガーベジコレクションの対象外です。
文字列は値なので、ガベージコレクション対象外となります。
__modeに何をセットしていたとしても、cacheTableのkeyもしくはvalueが、ガベージコレクションにより回収された場合、テーブル(サンプルのソースでいうとcacheTable)の項目一覧から要素が削除されます。
function showItem(t)
for key,value in pairs(t) do
print("key/value : ", key, value)
end
end
-- 何かのキャッシュをするテーブルとする
local cacheTable = {}
local cacheTableMeta = {
__mode="k" -- __modeには"k"(keyが弱い参照), "v"(valueが弱い参照), "kv"(keyもvalueも弱い参照)が指定できる。
}
setmetatable(cacheTable, cacheTableMeta)
local tmp1Data = {
-- 何かのデータ・・・
}
local tmp2Data = {
-- 何かのデータ・・・
}
local tmp1DataValue = {
-- 何かのキャッシュのようなデータ・・・
}
local tmp2DataValue = {
-- 何かのキャッシュのようなデータ・・・
}
-- cacheTableに値をセット
cacheTable[tmp1Data] = tmp1DataValue
cacheTable[tmp2Data] = tmp2DataValue
-- 何かの処理の結果tmp1Dataのデータが必要無くなりnilがセットされた・・・
tmp1Data = nil
-- 現在のcacheTableを表示
print("■ガベージコレクション実行前のcacheTable")
showItem(cacheTable)
-- ガベージコレクションを実行
collectgarbage()
print("■ガベージコレクション実行後のcacheTable")
showItem(cacheTable)
実行結果
■ガベージコレクション実行前のcacheTable key/value : table: 01194778 table: 01194818 key/value : table: 011947A0 table: 01194840 ■ガベージコレクション実行後のcacheTable key/value : table: 011947A0 table: 01194840
エラーハンドリング
luaスクリプトにエラー(文法エラーなども含めたその他もろもろ)が発生すると、その時点で処理が終了します。
しかし、終了したくない時もあります。
そういったケースのため、関数を実行し、エラーがあった場合は処理を呼び出し元に戻すことができる仕組みがあります。
pcall関数やxpcall関数を使用すると、引数で指定した関数を実行し、実行した結果がエラー無く終わったか、エラーがあったか教えてくれます。
ここではxpcall関数の使用例を示します(xpcallは、エラー発生時のエラーハンドリング関数を指定できます)。
サンプルソース中にもありますが、error関数を使うと故意にエラーを発生させることができます。
function f1(a, b)
print("f1 called", a, b)
-- 故意にエラーを発生させる
error("エラーです!")
end
function errHandler(x)
local msg = "エラーが発生しました : "..x
return msg
end
-- xpcall関数の第一引数に呼び出す対象の関数、第二引数にエラーハンドラの関数、第三引数以降にf1の引数をセットする
local callResult, errorMsg = xpcall(f1, errHandler, 1, 2)
print("callResult : ", callResult, errorMsg)
-- callResultがtrueの場合はエラーが発生していない状態
if callResult then
print("f1の処理は正常に終了しました")
else
print("f1の処理内部でエラーが発生しました")
end
実行結果
f1 called 1 2 callResult : false エラーが発生しました : study18.lua:8: エラーです! f1の処理内部でエラーが発生しました