5

tl; dr:グローバルテーブルに影響を与えることなく、いくつかの情報を共有する必要がある複数のファイルにLuaコードを分割できるデザインパターンは何ですか?

バックグラウンド

ライブラリの要求がグローバル名前空間に影響を与えるLuaでライブラリを作成することは、悪い形式と見なされます。

--> somelib.lua <--
SomeLib = { ... }

--> usercode.lua <--
require 'somelib'
print(SomeLib) -- global key created == bad

代わりに、ローカル変数を使用するライブラリを作成し、ユーザーが適切と考えるように割り当てるためにそれらを返すことがベストプラクティスと見なされます。

--> somelib.lua <--
local SomeLib = { ... }
return SomeLib

--> usercode.lua <--
local theLib = require 'somelib' -- consumers name lib as they wish == good

上記のパターンは、単一のファイルを使用する場合に正常に機能します。ただし、相互に参照する複数のファイルがある場合、これはかなり困難になります。

具体例

アサーションがすべて合格するように、次の一連のファイルをどのように書き直すことができますか?理想的には、書き換えによってディスク上に同じファイルが残り、各ファイルの責任が残ります。(すべてのコードを1つのファイルにマージして書き直すことは効果的ですが、役に立ちません;)

--> test_usage.lua <--
require 'master'

assert(MASTER.Simple)
assert(MASTER.simple)
assert(MASTER.Shared)
assert(MASTER.Shared.go1)
assert(MASTER.Shared.go2)
assert(MASTER.Simple.ref1()==MASTER.Multi1)
assert(pcall(MASTER.Simple.ref2))
assert(_G.MASTER == nil)                   -- Does not currently pass 

 

--> master.lua <--
MASTER = {}
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'

--> simple.lua <--
MASTER.Simple = {}
function MASTER:simple() end

--> multi.lua <--
MASTER.Multi1 = {}
MASTER.Multi2 = {}

--> shared1.lua <--
MASTER.Shared = {}

--> shared2.lua <--
function MASTER.Shared:go1() end

--> shared3.lua <--
function MASTER.Shared:go2() end

--> reference.lua <--
function MASTER.Simple:ref1() return MASTER.Multi1 end
function MASTER.Simple:ref2() MASTER:simple()      end

失敗:環境の設定

自己参照を使用して環境をマスターテーブルに設定することで、問題を解決しようと考えました。requireただし、次のような関数を呼び出すと、環境が元に戻るため、これは機能しません。

--> master.lua <--
foo = "original"
local MASTER = setmetatable({foo="captured"},{__index=_G})
MASTER.MASTER = MASTER
setfenv(1,MASTER)
require 'simple'

--> simple.lua <--
print(foo)         --> "original"
MASTER.Simple = {} --> attempt to index global 'MASTER' (a nil value)
4

5 に答える 5

4

あなたはmaster.luaに2つの責任を与えています:

  1. 共通モジュールテーブルを定義します
  2. すべてのサブモジュールをインポートします

代わりに、(1)用に個別のモジュールを作成し、それをすべてのサブモジュールにインポートする必要があります。

--> common.lua <--
return {}

--> master.lua <--
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'
return require'common' -- return the common table

--> simple.lua <--
local MASTER = require'common' -- import the common table
MASTER.Simple = {}
function MASTER:simple() end

最後に、の最初の行を変更しtest_usage.luaてローカル変数を使用します。

--> test_usage.lua <--
local MASTER = require'master'
...

これでテストに合格するはずです。

于 2013-02-19T16:38:14.000 に答える
3

私にはその問題を解決する体系的な方法があります。モジュールがどのように機能するかを示すために、Gitリポジトリでモジュールをリファクタリングしました:https ://github.com/catwell/dont-touch-global-namespace/commit/34b390fa34931464c1dc6f32a26dc4b27d5ebd69

アイデアは、サブパーツがメインモジュールを引数として取る関数を返すようにする必要があるということです。

master.luaでソースファイルを開いてチートし、ヘッダーとフッターを追加して使用する場合は、変更せずに使用することもできます( master.lualoadstringのみを変更する必要がありますが、より複雑です)。個人的には、それを明示的に保つことを好みます。これは私がここで行ったことです。私は魔法が好きではありません:)

編集:サブモジュールに直接MASTERテーブルにパッチを適用することを除いて、AndrewStarkの最初のソリューションに非常に近いです。利点は、 simple.luamulti.luareference.luaファイルのように、一度に複数のものを定義できることです。

于 2013-02-19T10:56:25.610 に答える
1

マスターファイルを変更して、必要なすべてのコードが実行される環境を変更することで、問題を解決できます。

--> master.lua <--
local m = {}                        -- The actual master table
local env = getfenv(0)              -- The current environment
local sandbox = { MASTER=m }        -- Environment for all requires
setmetatable(sandbox,{__index=env}) -- ...also exposes read access to real env

setfenv(0,sandbox)                  -- Use the sandbox as the environment
-- require all files as before
setfenv(0,env)                      -- Restore the original environment

return m

sandbox空のテーブルであり、値を継承します_Gが、テーブルへの参照もあり、MASTER後のコードの観点からグローバルをシミュレートします。このサンドボックスを環境として使用すると、後ですべて、このコンテキストで「グローバル」コードを評価する必要があります。

後で復元するために実際の環境を保存するので、実際にグローバル変数を設定する可能性のある後のコードを混乱させることはありません。

于 2013-02-18T18:17:49.680 に答える
1

質問は次のとおりです。

  1. モジュールを作成するときにグローバルスペースを汚染しません。
  2. とりわけメンテナンス上の理由から、モジュールを複数のファイルに分割できるようにモジュールを作成します。

上記の問題に対する私の解決策は、Luaの「テーブルとして返す」イディオムを微調整して、サブモジュール間で状態を渡す必要があるときに、テーブルを返す代わりにテーブルを返す関数を返すようにすることです。

これは、一部のルートモジュールに完全に依存しているサブモジュールに適しています。それら個別にロードされる場合、ユーザーは、モジュールを使用する前にモジュールを呼び出す必要があることを知っている必要があります。これは、メソッドのコレクションがあり、からすぐに使用できる他のすべてのモジュールとは異なりますlocal a = require('a')

とにかく、これは次のように機能します。

--callbacks.lua a -- sub-module
return function(self)
    local callbacks = {}
    callbacks.StartElement =  function(parser, elementName, attributes)
        local res = {}
            local stack = self.stack

    ---awesome stuff for about 150 lines...

    return callbacks
end

それを使用するには、次のことができます...

local make_callbacks = require'callbacks'
self.callbacks = make_callbacks(self)

または、次のように、コールバックテーブルを親モジュールに割り当てるときにrequireの戻り値を呼び出すだけです。

self.callbacks = require'trms.xml.callbacks'(self)

ほとんどの場合、私はこれをしないようにしています。サブモジュール間で状態または自己を渡している場合、それを間違って実行していることがよくあります。私の内部ポリシーは、別のファイルとの関連性が高いことをしている場合は、大丈夫かもしれないということです。おそらく、私は何かを間違った場所に置いており、モジュール間で何も渡さずにそれを行う方法があります。

これが気に入らない理由は、テーブルを渡すと、作業中のファイルにメソッドとプロパティが表示されないためです。私は、他のファイルをホークすることなく、自分のファイルの1つの内部実装を自由にリファクタリングすることはできません。ですから、このイディオムは黄色い旗ですが、おそらく赤い旗ではないことを謙虚に提案します。:)

これはグローバルなしの状態共有の問題を解決しますが、ユーザーをの偶発的な省略から実際に保護するわけではありませんlocal。私がその暗黙の質問に話すことができれば...

私が最初に行うことは、モジュールからグローバル環境へのアクセスを削除することです。それは私がリセットしない限り利用可能であることを思い出して、それをリセット_ENVすることは私が最初にすることです。これは、必要なものだけを新しい_ENVテーブルにパックすることによって行われます。

_ENV = {print = print, 
    pairs = pairs, --etc
}

ただし、luaから各ファイルに必要なものすべてを絶えず再入力することは、エラーが発生しやすい巨大な問題です。これを回避するために、モジュールのベースディレクトリに1つのファイルを作成し、それをすべてのモジュールおよびサブモジュールの共通環境のホームとして使用します。私はそれを呼びます_ENV.lua

注:「init.lua」またはその他のルートモジュールをこの目的に使用することはできません。これは、サブモジュールをロードするルートモジュールによってロードされるサブモジュールからロードできる必要があるためです。モジュール、それは...

私の省略_ENV.luaファイルは次のようになります。

--_ENV.lua
_ENV = {
    type = type,  pairs = pairs,  ipairs = ipairs,  next = next,  print =
    print,  require = require, io = io,  table = table,  string = string,        
    lxp = require"lxp", lfs = require"lfs",
    socket = require("socket"), lpeg = require'lpeg', --etc..
}
return _ENV

このファイルを使用して、作業を行うための共通ベースができました。他のすべてのモジュールは、次のコマンドを使用して、これを最初にロードします。

 _ENV = require'root_mod._ENV' --where root_mod is the base of my module.

この機能は、2つの理由で私にとって重要でした。まず、それは私をグローバルな空間から遠ざけます。地球環境から何かが足りないことがわかった場合_G(文字列がないことがわかるまでに驚くほど長い時間がかかりました!)、_ENV.luaファイルに戻って追加できます。必須ファイルとして、これは1回だけ読み込まれるため、すべてのサブモジュールに適用すると0カロリーになります。

次に、「テーブルとしてモジュールを返す」プロトコルを使用するために本当に必要なすべてのものが得られることがわかりました。ただし、「テーブルを返す関数を返す」が必要な場合は例外がいくつかあります。

于 2013-02-19T05:12:10.593 に答える
1

TL; DR: モジュールを設定しないでください 。できるだけ早く設定して(空のままでもかまいません)、サブモジュール内のモジュールだけを設定すると、適切に共有されます。returnpackage.loaded[...] = your_modulerequire


requireこれを行うためのクリーンな方法は、モジュールを明示的に登録し、最後に暗黙的に登録することに依存しないことです。ドキュメントには次のように書かれています。

require (modname)

指定されたモジュールをロードします。この関数は、 package.loadedテーブルを調べて、modnameすでにロードされているかどうかを判断することから始まります。そうである場合は、にrequire格納されている値を返します package.loaded[modname] [これにより、すべてのファイルが1回だけ実行されるというキャッシュ動作が得られます。]それ以外の場合は、モジュール のローダー を見つけようとします。[そして、検索者の1人が実行するLuaファイルを探しています。これにより、通常のファイル読み込み動作が得られます。]

[…]

ローダーが見つかったら、require2つの引数を使用してローダーを呼び出します modname。ローダーを取得した方法に応じて追加の値を指定します。(ローダーがファイルからのものである場合、この追加の値はファイル名です。)ローダーがnil以外の値を返す場合(たとえば、ファイルreturnがモジュールテーブルのrequire場合) 、戻り値をに割り当てます package.loaded[modname]。ローダーがnil以外の値を返さず、に値を割り当て package.loaded[modname]requireていない場合は、trueこのエントリに割り当てます。 いずれの場合も、requireの最終値を返します package.loaded[modname]

強調[コメント]私が追加しました。)

このreturn mymoduleイディオムでは、依存関係にループがある場合、キャッシュの動作は失敗します。キャッシュの更新が遅すぎます。(その結果、ファイルが数回ロードされ(無限ループになる可能性もあります!)、共有が失敗する可能性があります。)しかし、明示的に言う

local _M = { }           -- your module, however you define / name it
package.loaded[...] = _M -- recall: require calls loader( modname, something )
                 -- so `...` is `modname, something` which is shortened
                 -- to just `modname` because only one value is used

requireキャッシュをすぐに更新して、メインチャンクがreturn編集される前に他のモジュールがすでにモジュールを使用できるようにします。(もちろん、その時点では、実際にはすでに定義されているものしか使用できません。しかし、通常は問題にはなりません。)

このpackage.loaded[...] = mymoduleアプローチは5.1〜5.3(LuaJITを含む)で機能します。


あなたの例では、の開始を調整しmaster.luaます

1c1,2
< MASTER = {}
---
> local MASTER = {}
> package.loaded[...] = MASTER

および他のすべてのファイル

0a1
> local MASTER = require "master"

これで完了です。

于 2017-06-15T03:30:20.550 に答える