---
title: "トップティップス：Luaにおけるユニークなコンセプトと落とし穴の見分け方 - API7.ai"
description: "以下是一些独特的Lua概念，例如Weak Table、Closure和upvalue。"
url: "https://api7.ai/ja/learning-center/openresty/lua-basic-concepts-and-pitfalls"
---

# トップティップス：Luaにおけるユニークなコンセプトと落とし穴の見分け方

API7.ai

October 12, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

前回の記事では、LuaJITのテーブル関連ライブラリ関数について学びました。これらの一般的な関数に加えて、今日はOpenRestyにおけるLuaの独特な概念やよくある落とし穴を紹介します。

## 弱いテーブル（Weak Table）

まず、Luaの独特な概念である\_弱いテーブル\_についてです。これはガベージコレクションに関連しています。他の高級言語と同様に、Luaは自動的にガベージコレクションを行います。実装を気にする必要はなく、明示的にGCを呼び出す必要もありません。ガベージコレクタは、参照されていないスペースを自動的に回収します。

しかし、単純な参照カウントだけでは不十分な場合があり、より柔軟なメカニズムが必要です。例えば、Luaオブジェクト`Foo`（\_テーブル\_や関数）をテーブル`tb`に挿入すると、そのオブジェクト`Foo`への参照が作成されます。`Foo`への他の参照がなくなっても、`tb`内の参照は常に存在するため、GCは`Foo`が占有するメモリを回収できません。この場合、私たちには2つの選択肢しかありません。

- 1つは、`Foo`を手動で解放することです。
- 2つ目は、メモリ内に常駐させることです。

例えば、以下のコードです。

```
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
print(#tb) -- 2

collectgarbage()
print(#tb) -- 2

table.remove(tb, 1)
print(#tb) -- 1
```

しかし、使用していないオブジェクトがメモリを占有し続けるのは望ましくないでしょう。特にLuaJITには2Gのメモリ制限があります。手動での解放タイミングは容易ではなく、コードの複雑さを増します。

そこで\_弱いテーブル\_の出番です。その名前の通り、\_弱いテーブル\_はまずテーブルであり、その中のすべての要素が弱い参照です。概念は常に抽象的ですので、少し修正したコードを見てみましょう。

```
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "v"})
print(#tb)  -- 2

collectgarbage()
print(#tb) -- 0
'
```

使用されていないオブジェクトが解放されていることがわかります。最も重要なのは以下のコードです。

```
setmetatable(tb, {__mode = "v"})
```

見覚えがありますか？これはメタテーブルの操作です。そうです、メタテーブルに`__mode`フィールドがある場合、そのテーブルは\_弱いテーブル\_です。

- `__mode`の値が`k`の場合、テーブルのキーは弱い参照です。
- `__mode`の値が`v`の場合、テーブルの値は弱い参照です。
- もちろん、`kv`に設定することもでき、このテーブルのキーと値の両方が弱い参照であることを示します。

これらの3つの\_弱いテーブル\_のいずれも、キーまたは値が回収されると、そのキーと値のオブジェクト全体が回収されます。

上記のコード例では、`__mode`の値は`v`で、`tb`は配列であり、配列の値はテーブルと関数オブジェクトなので、自動的に回収されます。しかし、`__mode`の値を`k`に変更すると、解放されません。例えば、以下のコードを見てください。

```
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "k"})
print(#tb)  -- 2

collectgarbage()
print(#tb) -- 2
'
```

ここでは、値が弱い参照である\_弱いテーブル\_、つまり配列型の\_弱いテーブル\_のみをデモンストレーションしました。当然、オブジェクトをキーとして使用してハッシュテーブル型の\_弱いテーブル\_を構築することもできます。例えば、以下の通りです。

```
$ resty -e 'local tb = {}
tb[{color = red}] = "red"
local fc = function() print("func") end
tb[fc] = "func"
fc = nil

setmetatable(tb, {__mode = "k"})
for k,v in pairs(tb) do
     print(v)
end

collectgarbage()
print("----------")
for k,v in pairs(tb) do
     print(v)
end
'
```

手動で`collectgarbage()`を呼び出して強制的にGCを実行すると、`tb`テーブルのすべての要素が解放されます。もちろん、実際のコードでは`collectgarbage()`を手動で呼び出す必要はありません。バックグラウンドで自動的に実行され、私たちは気にする必要はありません。

しかし、`collectgarbage()`関数について言及したので、もう少し説明します。この関数にはいくつかの異なるオプションを渡すことができ、デフォルトは`collect`で、これは完全なGCです。もう1つ有用なのは`count`で、Luaが占有するメモリ空間の量を返します。この統計はメモリリークがあるかどうかを確認するのに役立ち、2Gの上限に近づかないように注意を促します。

\_弱いテーブル\_に関連するコードは実際に書くのが複雑で、理解しにくく、それに応じて隠れたバグも多くなります。急ぐ必要はありません。後で、\_弱いテーブル\_によるメモリリーク問題を紹介するオープンソースプロジェクトを紹介します。

## クロージャとupvalue

次に、クロージャとupvalueについてです。以前にも強調したように、Luaではすべての値が第一級市民であり、関数も含まれます。これは、関数を変数に格納したり、引数として渡したり、別の関数の値として返したりできることを意味します。例えば、上記の\_弱いテーブル\_のサンプルコードに登場した以下のコードです。

```
tb[2] = function() print("func") end
```

これは匿名関数で、テーブルの値として格納されています。

Luaでは、以下のコードの2つの関数の定義は同等です。ただし、後者は関数を変数に代入する方法で、私たちがよく使う方法です。

```
local function foo() print("foo") end
local foo = fuction() print("foo") end
```

さらに、Luaは関数内に関数を書くことをサポートしています。つまり、ネストされた関数です。例えば、以下のサンプルコードです。

```
$ resty -e '
local function foo()
     local i = 1
     local function bar()
         i = i + 1
         print(i)
     end
     return bar
end

local fn = foo()
print(fn()) -- 2
'
```

`bar`関数が`foo`関数内のローカル変数`i`を読み取り、その値を変更できることがわかります。これは、`bar`内で定義されていない変数でも可能です。この機能はレキシカルスコープと呼ばれます。

Luaのこれらの機能はクロージャの基礎です。クロージャは、単に別の関数のレキシカルスコープ内の変数にアクセスする関数です。

定義上、Luaのすべての関数は実際にはクロージャです。ネストしていなくてもです。これは、LuaコンパイラがLuaスクリプトの外側を取り、別の層のメイン関数でラップするためです。例えば、以下の簡単なコードです。

```
local foo, bar
local function fn()
     foo = 1
     bar = 2
end
```

コンパイル後、以下のようになります。

```
function main(...)
     local foo, bar
     local function fn()
         foo = 1
         bar = 2
     end
end
```

そして、関数`fn`はメイン関数の2つのローカル変数をキャプチャするため、これもクロージャです。

もちろん、クロージャの概念は多くの言語に存在し、Luaに固有のものではありません。比較対照することで、より理解を深めることができます。クロージャを理解して初めて、`upvalue`について話すことができます。

`upvalue`はLuaに固有の概念で、クロージャ内でキャプチャされたレキシカルスコープ外の変数です。上記のコードを続けて見てみましょう。

```
local foo, bar
local function fn()
     foo = 1
     bar = 2
end
```

関数`fn`が、自身のレキシカルスコープ内にない2つのローカル変数`foo`と`bar`をキャプチャしていることがわかります。これらの変数は、実際には関数`fn`の`upvalue`です。

## よくある落とし穴

Luaのいくつかの概念を紹介した後、OpenResty開発で遭遇したLua関連の落とし穴について話します。

前回のセクションでは、Luaと他の開発言語の違いについて触れました。例えば、インデックスが`1`から始まること、デフォルトでグローバル変数であることなどです。OpenRestyの実際のコード開発では、LuaとLuaJITに関連する問題に遭遇することが多く、以下にいくつかの一般的なものを紹介します。

ここで注意点として、すべての落とし穴を知っていても、自分で実際に踏んでみないと印象に残らないでしょう。違いは、穴から這い出し、問題の核心を見つける方法がより良くなることです。

### インデックスは0からか1からか？

最初の落とし穴は、Luaのインデックスが`1`から始まることです。これは以前にも繰り返し述べました。

しかし、これは完全な真実ではありません。なぜなら、LuaJITでは`ffi.new`で作成された配列は再び`0`からインデックスされるからです：

```
local buf = ffi_new("char[?]", 128)
```

したがって、上記のコードの`buf` `cdata`にアクセスする場合、インデックスが`0`から始まることを覚えておいてください。FFIを使用してCとやり取りする際に、この点に特に注意してください。

### 正規表現マッチ

2つ目の落とし穴は、正規表現マッチの問題です。OpenRestyには、LuaのstringライブラリとOpenRestyの`ngx.re.*` APIという2つの文字列マッチング方法が並行して存在します。

Luaの正規表現マッチは独自の形式であり、PCREとは異なる書き方です。以下は簡単な例です。

```
resty -e 'print(string.match("foo 123 bar", "%d%d%d"))'  — 123
```

このコードは文字列から数値部分を抽出しますが、私たちが慣れ親しんだ正規表現とは全く異なることがわかります。Luaの正規表現マッチングライブラリは保守コストが高く、パフォーマンスも低いです。JITはこれを最適化できず、一度コンパイルされたパターンはキャッシュされません。

したがって、Luaの組み込みstringライブラリを使用して`find`や`match`などを行う場合、正規表現のようなものが必要な場合は、ためらわずにOpenRestyの`ngx.re`を使用してください。固定文字列を探す場合、stringライブラリをプレーンモードで呼び出すことを検討します。

以下は提案です：OpenRestyでは、常にOpenRestyのAPIを優先し、次にLuaJITのAPIを使用し、Luaライブラリは慎重に使用します。

### JSONエンコードは配列とdictを区別しない

3つ目の落とし穴は、JSONエンコードが配列とdictを区別しないことです。Luaにはデータ構造が1つしかないため、\_テーブル\_が空の場合、JSONエンコード時にそれが配列か辞書かを判断する方法がありません。

```
resty -e 'local cjson = require "cjson"
local t = {}
print(cjson.encode(t))
'
```

例えば、上記のコードは`{}`を出力します。これは、OpenRestyの`cjson`ライブラリがデフォルトで空のテーブルを辞書としてエンコードすることを示しています。もちろん、`encode_empty_table_as_object`関数を使用してこのグローバルデフォルトを変更できます。

```
resty -e 'local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))
'
```

今回は、空のテーブルが配列`[]`としてエンコードされます。

しかし、このグローバル設定は大きな影響を与えるため、特定のテーブルのエンコードルールを指定することはできるでしょうか？答えはもちろんイエスで、2つの方法があります。

1つ目の方法は、指定されたテーブルに`userdata` `cjson.empty_array`を割り当てることです。これにより、JSONエンコード時に空の配列として扱われます。

```
$ resty -e 'local cjson = require "cjson"
local t = cjson.empty_array
print(cjson.encode(t))
'
```

しかし、指定されたテーブルが常に空であるかどうかわからない場合もあります。空の場合に配列としてエンコードしたい場合は、`cjson.empty_array_mt`関数を使用します。これが2つ目の方法です。

これは指定されたテーブルをマークし、テーブルが空の場合に配列としてエンコードします。`cjson.empty_array_mt`という名前からわかるように、これは`metatable`を使用して設定されます。以下のコード操作のようにです。

```
$ resty -e 'local cjson = require "cjson"
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))
'
```

### 変数の数の制限

4つ目の落とし穴は、変数の数の制限です。Luaには、関数内のローカル変数の数と`upvalue`の数に上限があります。Luaのソースコードから確認できます。

```
/*
@@ LUAI_MAXVARSは関数ごとのローカル変数の最大数です
@*（250未満でなければなりません）。
*/
#define LUAI_MAXVARS            200

/*
@@ LUAI_MAXUPVALUESは関数ごとのupvalueの最大数です
@*（250未満でなければなりません）。
*/
#define LUAI_MAXUPVALUES        60
```

これらの2つの閾値は、それぞれ`200`と`60`にハードコードされています。ソースコードを手動で修正してこれらの値を調整することはできますが、最大でも`250`にしか設定できません。

一般的に、この閾値を超えることはありません。しかし、OpenRestyのコードを書く際には、ローカル変数と`upvalue`を過剰に使用しないように注意し、`do ... end`をできるだけ使用してローカル変数と`upvalue`の数を減らすようにしてください。

例えば、以下の疑似コードを見てみましょう。

```
local re_find = ngx.re.find
function foo() ... end
function bar() ... end
function fn() ... end
```

もし関数`foo`だけが`re_find`を使用する場合、以下のように修正できます：

```
do
    local re_find = ngx.re.find
    function foo() ... end
end
function bar() ... end
function fn() ... end
```

## まとめ

「もっと質問する」という観点から、Luaの`250`という閾値はどこから来たのでしょうか？これが今日の思考問題です。コメントを残して、この記事を同僚や友人と共有してください。一緒にコミュニケーションを取って改善していきましょう。

Share article link

Share

Related Posts

[![OpenRestyの特別な点は何か](https://static.api7.ai/2022/10/14/634974e090ab4.png)](https://api7.ai/learning-center/openresty/openresty-getting-started)

## [OpenRestyの特別な点は何か](https://api7.ai/learning-center/openresty/openresty-getting-started)

[![LuaルールとNGINX設定が競合した場合どうなるか？](https://static.api7.ai/2022/10/14/634974e117ebd.png)](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

## [LuaルールとNGINX設定が競合した場合どうなるか？](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

Table of Contents

- [弱いテーブル（Weak Table）](#%E5%BC%B1%E3%81%84%E3%83%86%E3%83%BC%E3%83%96%E3%83%ABweak-table)

- [クロージャとupvalue](#%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3%E3%81%A8upvalue)

- [よくある落とし穴](#%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4)

  - [インデックスは0からか1からか？](#%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AF0%E3%81%8B%E3%82%89%E3%81%8B1%E3%81%8B%E3%82%89%E3%81%8B)
  - [正規表現マッチ](#%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE%E3%83%9E%E3%83%83%E3%83%81)
  - [JSONエンコードは配列とdictを区別しない](#json%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AF%E9%85%8D%E5%88%97%E3%81%A8dict%E3%82%92%E5%8C%BA%E5%88%A5%E3%81%97%E3%81%AA%E3%81%84)
  - [変数の数の制限](#%E5%A4%89%E6%95%B0%E3%81%AE%E6%95%B0%E3%81%AE%E5%88%B6%E9%99%90)

- [まとめ](#%E3%81%BE%E3%81%A8%E3%82%81)
