Lua 函式模組的批評

lua-users home
wiki

本文所提出的論點是,Lua 5.1 的module 函式 [1] 存在設計上的缺陷,這會鼓勵不良的模組設計手法,可能會透過全域變數的副作用導致程式錯誤和歧義,因此應避免使用此函式。希望本文能進一步阻止使用module 函式,並在未來的 Lua 版本中移除或改進此函式。

(我們了解有支持此觀點、反對此觀點,以及漠不關心的人,例如 [15] 串中的討論。)

在詳細說明 module 函式的危險之前,我們會注意到,選擇是否使用 module 函式並不僅僅是個人選擇,它還將影響其他作者。Lua 模組作者很容易避免撰寫 module 呼叫。的確,此函式對於定義模組來說並非必要,因為它只是一個簡單的輔助函式,它封裝了本身不受 Lua 或其他有用得多的 Lua 5.1 模組系統部分(例如 require)所需要的共用行為。[*A] 然而,由於模組經常使用其他作者撰寫的模組,而這些作者自己可能使用了 module 函式,而且 module 函式會造成全域副作用,因此並非完全能夠選擇避免其影響,而無需修改那些其他模組的實作。實際上, module 函式是相當常見的,這可能是由於 module 函式包含在 Lua 標準函式庫中,可能是作為方便且標準化的模組定義最佳範例,而多數官方或有信譽的 Lua 資源,例如 Lua 參考手冊 [2] 和使用 Lua 進行程式設計 (PiL) [3] 都鼓勵使用 module 函式,甚至表示它是一個好選擇。因此,新使用者會快速習慣使用 module 函式。

使用 module 函式定義模組的慣常方式如下

-- hello/world.lua
module(..., package.seeall)
local function test(n) print(n) end
function test1() test(123) end
function test2() test1(); test1() end

且使用方式如下

require "hello.world"
require "anothermodule"
hello.world.test2()

針對 module 函式提出了兩個主要抱怨,如果 anothermodule 的定義如下所示,便可看出這兩個抱怨

-- anothermodule.lua
module(..., package.seeall)
assert(hello.world.hello.world.print == _G.print)  -- weird
assert(hello ~= nil) -- where'd this come from anyway?

首先,可以透過索引模組表格來存取全域命名空間;其次,即使未請求 hello,它仍會在此模組中顯示。

第一個抱怨不是由於 inherent 的 module 函式,而是僅由於 package.seeall 選項所致。package.seeall 允許模組看到全域變數,而這些變數通常都是隱藏的,因為 module 函式會使用一個本機環境取代模組的目前環境。package.seeall 所做的,就是修改模組環境的元表,將其退回 _G。不只允許模組本身存取 _G,而 _G 中的變數也會成為模組介面的一部分。在各種事物中,透過模組表格揭露全域環境的行為可能對沙盒不利(請參閱 SandBoxes),而且這些變數可能意外地被使用,但更明顯的是,那只是純粹奇怪的行為而已。

幸運的是,package.seeall 僅為簡便選項,而且可以避免使用

-- hello/world.lua
local _G = _G
module(...)
function test() _G.print(123) end

-- hello/world.lua
local print = print
module(...)
function test() print(123) end

這些有點尷尬,但可能還有其他在語法上更令人愉快的避免方式,例如辨識模組表格和模組環境表格不需要相同(例如,請參閱 ModuleDefinition -- 「具有公用/私人名稱空間的模組系統」)。我們不會在這第一點上進行更多細節說明。

第二個重點在於 module 函式具有明確的副作用,會以程式設計師無法完全控制的方式建立全域變數名稱。在執行 module("hello.world") 時,此函式會在全域環境(初始的全域環境,而非透過 setfenv 設定的目前環境)中建立一個名為 "hello" 的表格,並將模組表格儲存在該表格的 "world" 金鑰之下。不過,如果其中任何變數已經存在(例如有人將它們放在那裡),則此函式會產生並產生錯誤,這至少會提供某些程度的安全性。模組函數的行為透過 Lua 中的以下表示法(取自 LuaCompat [4])最能理解(實際版本在 loadlib.c 中)。

local _LOADED = package.loaded
function _G.module (modname, ...)
  local ns = _LOADED[modname]
  if type(ns) ~= "table" then
    ns = findtable (_G, modname)
    if not ns then
      error (string.format ("name conflict for module '%s'", modname))
    end
    _LOADED[modname] = ns
  end
  if not ns._NAME then
    ns._NAME = modname
    ns._M = ns
    ns._PACKAGE = gsub (modname, "[^.]*$", "")
  end
  setfenv (2, ns)
  for i, f in ipairs (arg) do
    f (ns)
  end
end

因為我們有不同的人員維護不同的模組寫入全域環境,所以問題產生了。此外,使用這些模組的應用程式也可能會寫入全域環境。由於資訊隱藏,[5] 模組和應用程式不應知道這些模組的內部運作/實作,甚至可能連這些模組所需的模組名稱都不應知道。結果是程式無法控制哪些全域變數會被設定。下列說明由此產生的各種此類問題。

在以下範例中,我們將模組定義在內嵌程式中,而非將它們放在個別檔案中,以簡便為目的。例如,我們不會建立以下這類的兩個檔案

-- mymodule.lua
module(...)
function test() return 1+2 end

-- mymodule_test.lua
require "mymodule"
print(mymodule.test())

我們將會簡單寫成

(function()
  module("mymodule")
  function test() return 1+2 end
end)();
print(mymodule.test())

以下是第一個範例

(function()
  local require = require
  local print = print
  local module = module
  module("yourmodule");

  (function() module("mymodule") end)()

  print(mymodule ~= nil) -- prints false (where is it?)
end)();

print(mymodule ~= nil) -- prints true (where did this come from?)

如下所示,載入模組,例如「mymodule」,總是會設定全域環境,而不是模組被使用的當前環境。這與需求的相反。許多這樣的模組載入會以用於設定為私人變數的變數填滿全域環境。

另一個問題是,馬克·漢堡記下,[16]將模組放入全域命名空間中會隱藏相依關係。假設程式載入模組「bar」而且載入模組「bar」也會載入模組「foo」。現在模組「foo」也會在全域命名空間中可用。在程式中,你開始在全域命名空間中使用模組「foo」。如果模組「bar」現在移除模組「foo」的相依關係,它也不會在全域命名空間中可用,並終止你的程式。你並不立刻顯現全域命名空間中的foo來自何處,也不是實際上是模組 (這以前是模組「bar」的相依關係)。

下列兩個範例彼此相關

function test() return 1+2 end

(function()
  module("mymodule", package.seeall);

  (function()
    module("test.more") -- fails: name conflict for module 'test.more'
    function hello() return 1+2 end
  end)()
end)()

(function()
  module("test")
  function check() return true end
end)();

(function()
  module("test.check") -- fails: name conflict for module 'test.check'
  function hello() return 1+2 end
end)();

如你所見,套件名稱和一般變數名稱會衝突。模組函數確實會偵測並提出錯誤,如果全域變數它改寫已經存在。那是我們想要的,對吧?好,這也表示特別不確定載入模組是否會成功,因為模組可能會載入其名稱 (及其成員名稱) 的其他模組,我們可能不知道,而且與全域變數衝突。

補充說明,在某些其他語言中 (例如 Perl) 變數和套件名稱會在個別命名空間中維護,因此防止衝突。[*3] 也請注意,模組命名規則會影響名稱是否衝突以及如何衝突。例如,Java 套件名稱 [6] 慣例會加上作者控制下的 (唯一) 網域名稱的前置詞,雖然較冗長,但是提供機制來避免衝突。在 Perl 中,CPAN 提供中心命名註冊表來防止衝突,而具有相同前置詞的模組會表示共用功能,而不是共用維護者 (例如「CGI」[7] 和「CGI::Minimal」[8] 由不同的作者獨立維護,而且「CGI::Minimial」不是儲存在「CGI」表格中)。

(function()
  module("mymodule", package.seeall);

  (function()
    module("test.more")
    function hello() return 1+2 end
  end)()

  function greet()
    test.more.hello()  -- fails -- attempt to index global 'test' (a function value)
  end
end)();

function test()
  mymodule.greet()
end

test()

在此,程式意外改寫模組函數所設定的全域變數。模組函數不會偵測到這點。相反地,如果是相依於此全域變數的模組嘗試存取此變數,就會出現程式失敗 (可能是中斷)。

(function()
  local require = require
  local module = module
  local print = print
  local _P = package.loaded
  module('yourmodule.two');

  (function()
    module('mymodule.one')
  end)()

  print(_P['mymodule.one'] ~= nil) -- prints true
end)();

local _P = package.loaded
print(_P['mymodule.one'] ~= nil) -- prints true

事實上,將模組儲存在全域環境是有些多餘,因為它們也儲存在 package.loaded 中(雖然沒有為模組名稱中出現的句點建立巢狀表格)。

~~~

上述問題可以透過不使用模組函數,而依以下簡單的方式定義模組來避免: [*1][*2]

-- hello/world.lua
local M = {}

local function test(n) print(n) end
function M.test1() test(123) end
function M.test2() M.test1(); M.test1() end

return M

並以這種方式匯入模組

local MT = require "hello.world"
MT.test2()

請注意,公開函式會明確標示「M.」前綴。與使用「module」不同的地方在於,雖然全域環境不會透過「MT」表格(即「MT.print == nil」)顯示,但「hello.world」表格並未輸出(或汙染)至全域環境,而是一個字彙,而且具有相同前綴的模組(例如「hello.world.again」)不會變更「hello.world」表格。在客戶端程式碼中,可為模組「hello.world」賦予該模組專屬的簡寫(例如「MT」)。此方法也適用於DetectingUndefinedVariables。真是太棒了。唯一缺點是,在模組本身中,公開函式需要以「M.」作為前綴,但其他解決方案經常會提出自己的問題和複雜性,例如上述的「package.seeall」。明確表達「M.」(兩個字元)並不會造成什麼問題,特別是在程式碼大小變大的時候。

C 程式碼的相關注意事項:C 中的「luaL_register [9]」函式類似 Lua 中的「module」函式,因此「luaL_register」具有類似的問題,至少在使用非 NULL 的「libname」時。此外,「luaL_newmetatable」、「luaL_getmetatable」、「luaL_checkudata」函式會使用 C 字串作為全域註冊表的鍵值。這可能會造成名稱衝突,可能有兩個原因:模組由不同的人撰寫,或它們是同時載入的同一個模組的不同版本。為了解決這個問題,我們可以使用一個輕量級的使用者資料(指向靜態連結變數的指標),以確保全域唯一性,或將元表格儲存為變數,兩個方式都能更有效率,且較不容易出錯。

module」函式(及其類型)可能會造成的問題比解決的還多。

--DavidManura

註腳

[*1](上述樣式的提倡者包括RiciLakeDavidManura,以及在 IRC 上提過這個話題的其他人士,MikePall [17][18][19],...(請在此處填寫您的姓名)

[*2] 也有建議將標準函式庫朝這個方向移動 [20]

[*3] Perl 中的範例:模組與同名的變數不會造成衝突。

package One;
our $Two = 2;
package One::Two;
our $Three = 3;
package main;
print "$One::Two,$One::Two::Three" # prints 2,3

其他重點

以下的許多其他重點取自 2011 年 10 月有關模組的討論 [21][22]

套件

使用「module」函式定義模組時,如果需要將模組套件打包到單一檔案中,有時我們可以將它們串接(「cat *.lua > bundle.lua」)[21]。不過這在一般情況下無法執行。

module("one", package.seeall)
require "two"  -- This fails unless you sort the modules according to their dependency graph
               -- (assuming, as is best design, it has no cycles and can be computed statically)
local function foo() print 'one.foo' end
function bar() foo() two.foo() end

module("two", package.seeall)
function foo() print 'two.foo' end  -- This overwrite a previous local

module("main", package.seeall)
require "one"
one.bar()

適用於含和不含 module 的模組的通用解決方案涉及 package.preload,如下所示

package.preload['one'] = function()
  module("one", package.seeall)
  require "two"
  local function foo() print 'one.foo' end
  function bar() foo() two.foo() end
end

package.preload['two'] = function()
  module("two", package.seeall)
  function foo() print 'two.foo' end
end

package.preload['main'] = function()
  module("main", package.seeall)
  require "one"
  one.bar()
end

require 'main'

列在 BinToCee 底部的許多綑綁實用程式利用類似的做法。

在私有和公開之間切換

針對「M」表格樣式的模組定義受到的一項批評是,如果模組中的函式定義從公開變更為私有時,則對該函式的所有參照都必須重新命名 (例如,M.foo 變更為 foo) [23]

function M.foo() end          -- change to "local function foo() end"
function M.bar() M.foo() end  -- and also change "M.foo()" to "foo()"

減緩因素是,對 M.foo() 的參照是本機化到目前的模組的,而且通常數量很少。這裡需要進行的重構作業也和您想要重新命名函式時相同,而這在任何情況下都是必要的。文字編輯器可以協助進行此項重構,而某些瞭解 Lua 語言的編輯器也可以非常穩健地重新命名變數。在某些語言中 (例如 Python),私有變數通常使用下底線與公開變數進行區分,因此相同的批評也會適用。

要避免重新命名的一種做法是將所有函式都保持為本機函式,然後將任何應為公開函式的函式插入至公共表格中,就在其定義後面

local function foo() end; M.foo = foo

某些效能關鍵代碼需要這樣做,才能獲得微幅效能優勢。定義中三次使用 foo 是很遺憾的,而要避免此問題的變通方法 (例如 ModuleDefinition 中的 localmodule 或令牌篩選器) 可能不值得採用。

最後,請注意,將函式從公開 (表格或全域變數) 變更為私有 (本機變數) 時可能也需要移動函式定義。與表格或全域變數不同,本機變數是屬於詞彙層級範圍的,因此必須在使用前進行宣告 (或前向宣告)。不熟悉詞彙層級範圍的新使用者可能會因此感到糊塗。我們可以透過統一地使用 locals (如上述範例所示) 或表格/全域變數 (如下所示) 宣告所有變數 (公開和私有),來避免此問題。後者可以包含使用下底線為私有變數加上前綴或使用兩個表格的 Python 式方法

local M = {}
function M._foo() print 'foo' end
function M.bar() M._foo() end
return M

local M = {} -- public
local V = {} -- private
function V.foo() print 'foo' end
function M.bar() V.foo() end
return M

不過,其中任何一種方法都無法解決當一個函式從公開變更為私有或從私有變更為公開時,需要替換參照的問題。我們也可以透過使用像這樣的方法來解決此問題

local M = {} -- public
local V = setmetatable({}, {__index = M}) -- private and public
function V.foo() print 'foo' end
function M.bar() V.foo() end
return M

現在,我們可以始終透過只變更檔案中的一個字元,便能安全地將函式從公開變更為私有,或從私有變更為公開。如果我們想要避免部分雜亂內容,我們可以將部分內容移到模組載入器中,以便只須以以下方式撰寫模組

function V.foo() print 'foo' end
function M.bar() V.foo() end

可能不是最簡潔的內容,但公開和私有範圍 (V/M) 之間的區別很明確。

機制而非政策

「儘管我們有「機構,而非政策」的原則,對於指導 Lua 的演進極有價值,我們早該為模組和套件提供一套明確的政策。缺乏建立模組和安裝套件的通用政策,阻礙了不同群組分享程式碼,也抑制了社群程式碼庫的開發。Lua 5.1 提供了一套針對模組和套件的政策,希望可以解決這個問題。」-- Lua 演進史,https://lua.dev.org.tw/doc/hopl.pdf

「一般而言,Lua 沒有設定政策。取而代之的是,Lua 提供了強大到足以讓開發人員群組實作最適合自己的政策的機構。然而,這種方法對於模組而言並不好用。模組系統的主要目標之一,就是讓不同的群組能夠分享程式碼。缺乏共同政策妨礙了這種分享。」-- http://www.inf.puc-rio.br/~roberto/pil2/chapter15.pdf

較新的釐清:LuaList:2011-10/msg00485.html

請參閱 機構,而非政策

模組函式的使用普及度

目前在儲存庫中大部分的純 Lua 模組都使用 module 函式

在模組函式中使用「...」

LuaList:2011-10/msg00686.html 主張最好在模組文字中明確指定模組名稱,讓載入方式一目瞭然

module("foo.bar")    -- encouraged
module(...)          -- discouraged
-- module: foo.bar   -- name in informal comment better than nothing
local M = {} return M                        -- anonymous and likewise discouraged
local M = {}; M._NAME = "foo.bar"; return M  -- better than above

不過,這會使套件不易重新定位。

Lua 版本相容性

Lua 5.1 module 函式可透過 [LuaCompat] 在 Lua 5.0 中使用。Lua 5.2.0-beta 有相容模式,此外「在 5.2 中,使用偵錯函式庫撰寫「模組」函式是很容易的。(但它不允許在單一檔案中有多個模組,這有一點像是取巧的方式)」(Roberto,LuaList:2011-10/msg00488.html

「M」表格樣式的模組定義在 5.0、5.1 和 5.2.0-beta 中也相容。

_ENV 在 5.1 中不受直接支援,所以使用它會使模組無法與 5.1 相容。也許您可以使用 setfenv 和透過 __index/__newindex 來攔截對它的讀取/設定值,模擬 _ENV,或是乾脆避免使用 _ENV

Lua 5.2 模組定義

在 5.2.0-beta 中,您可以繼續使用「M」表格樣式的模組定義,也有人建議這麼做 [24]。但另一方面,有些人建議在 Lua 5.2 中使用新的 _ENV 變數 (它很大程度上取代了 setfenv) 來撰寫模組,如下所示

_ENV = module(...)
function foo() end

有些人主張新的使用者不需清楚難懂的_ENV。儘管當區塊被載入時,可以由其他人設定 _ENV來避免這個問題(例如 require 或搜尋器函式),但其他人則繼續辯稱,模組的私有環境和公用表格不應混合,而且根本不需要 _ENV

促進模組開發

LuaModuleFunctionCritiqued 中所提出的論點是 module 有技術缺陷(副作用和難懂的極端情況),阻礙了模組特性的核心性質:可組合性。在實際中,這表示一個應用程式載入兩個不同作者所寫的不同模組不應經歷到這兩個模組之間任何意外的互動。另一方面,「M」表格樣式(如果使用正確)沒有這些缺陷,因為它非常簡單語意沒有副作用(正式來說,模組載入器通常可以在函數式程式設計意義上被視為一個 [純函式] )。

Hisham 在 [25] 中主張,即使 module 有技術缺陷,但在促進模組定義的更標準政策這一點上,這些缺陷基本上可以被修復並且相對於它的成功而言,是微不足道的(5.0 沒有)。這種成功似乎出現在社會學層面,而不是技術層面。他們表示 module 促進了程式碼共享和社群程式碼庫的開發,而且大多數的 LuaRocks 模組都使用 module。此外, module 的使用(在一些其他語言中,它是一個內建關鍵字)有一個關注自文件特質,以最少的樣板程式碼宣告程式碼的用意(我是個模組,這是我的名稱,這裡是我的公用函式)。我們似乎也同意,模組中難懂的樣板程式碼(setfenv/_ENV)是可讀性的一個缺點。

然而,有人主張 5.1 模組系統推出幾年後,對於 Lua 模組的數量、品質和一致性,甚至對於像 StandardLibraries 這樣的基礎,仍然經常聽到抱怨。這場比賽中還有其他事實和努力,像是 LuaRocks 就針對這些領域中的一些進行改進,但比較困難的問題是區分出 module 是否有幫助或造成傷害,以及 5.2 中的變更是否會有幫助。

由於像 penlight [10] 這樣的「標準函式庫」已經從其執行移除 module 呼叫 [26],很明顯這不會對任何人造成負面影響。然而,事實是 Lua 5.2.0-beta 已經不建議使用 module,這表示目前已經使用 module 的模組在沒有載入相容函式或重寫的情況下可能無法再運作,已經造成一些隱憂 [27]。

混合全域和模組的命名空間

在將全域變數(如 print)和模組函式(如 foo.print)放在同一個命名空間(如 package.seeall 所做的那樣),模組函式可能會覆蓋同名的全域函式。這是部分原因,有些人偏好明確指定,給予模組函式一個獨特的開頭(如 M.print)。這樣也可能有助於可讀性,在於很明顯地指出 M.print 是從目前模組中匯出的公有變數,而不是一個本機、全域或匯入的套件名稱。有時這種開頭方式在程式碼的其他部分中還是有其必要性(例如:「self.」或「ClassName:」)。這樣的顯式做法也避免合併兩個命名空間和元表的任何問題或負擔。然而這種作法會引入一些重複(請參閱上文「在私有和公有之間切換」)。

關於像「using std;[11] 之類的事情,C++ 社群也有類似的爭論,它會將可能衝突的名稱匯入目前的命名空間,因此最保險的做法是避免使用。此外,在 C 中,有一種常見做法是在全域或靜態變數之前加上「g_」或「s_」(類似於在 C++ 中成員的「m_」),不過智能整合開發環境(IDE)可以減緩一些這方面的需求。

Python 和 Perl 名稱外洩

Python 有類似 package.seeall 的問題 [23]

-- bar.py
import logging # logging is now available as bar.logging

Perl 也有相同問題

package One;
use Carp qw(carp);  # carp is now available via One::carp
carp("test");
1

除非你避免符號匯入(改為完全限定名稱)

package One;
use Carp qw();  # carp is now available via One::carp
Carp::carp("test");
1

或使用 [namespace::clean][namespace::autoclean] หรือ [namespace::sweep] 模組。

Python(如 Lua module)在 foofoo.baz 模組之間施加了一種關係。如果你的模組載入 foo 而另一個模組載入 foo.baz,那麼 baz 將會被放入你的模組中。這可能是為什麼 Hitchhiker's Guide to Packaging [12] 建議模組名稱的第一部分(「foo.」)應該在全域中保持唯一,而且看來 Python 套件趨於只在套件是由同一個實體管理時,才共用同一個開頭(例如許多 Zope 套件/模組在同一個「zope.」開頭下,但不是全部)。在 Lua 的目前狀態中,應該套用相同的準則。

Perl 不是完全相同,因為你可以有一個包含變數 $Foo::Baz 的套件 Foo,還有一個套件 Foo::Bar,而它們不會衝突,因為套件存在於它們自己的命名空間中。但是,Perl 和 Lua module 共用下列問題:

# One.pm
package One; sub test { }; 1

# Two.pm
package Two; One::test(); 1

# main.pl
use One;
use Two;   # This succeeds as written but fails if the lines are later reversed

透過 package.loaded 的全域變數

Require 在 package.loaded 中全域註冊模組。因此,無論模組如何定義,只要你想要就仍然可以全域存取已載入的模組。

local L = package.loaded
.....
L['foo.bar'].baz()
L.foo.bar.baz()  -- if require'foo'.bar == require 'foo.bar'

這麼做可能會被認為很花力氣。

local FBAR = require 'foo.bar'
local FBAZ = require 'foo.baz'
local FBUZ = require 'foo.buz'
...

你可能只能接受它,或者你可以找到方法來簡化它。

-- foo.lua
return {
  bar = require 'foo.bar',
  baz = require 'foo.baz',
  buz = require 'foo.buz'
}

或者找到自動化它的方法。

local _G = require 'autoload' -- under appropriate definition of autoload
_G.foo.bar.qux()
-- note: penlight offers something like this

後者不會有遺失隱藏相依項的問題,因為如果需要,模組隨時都會載入。另一方面,模組載入不會局部到模組載入程式,反而會發生在任何使用模組程式的後續時段,這意味著錯誤偵測將會延後,而這些程式使用也較容易發生故障 (例如,因為模組未安裝而導致模組載入故障),複雜化了錯誤處理。不過這可以用幾種方法解決。

定義類別

一個與標準化模組定義相似的問題在於標準化類別定義 (物件導向程式)。而且,某些模組也是類別。

舉例來說,ratchet 的程式碼 [13] 做一些類似這樣的事

.....
module("ratchet.http.server", package.seeall)
local class = getfenv()  -- why not _M?
__index = class

function new(socket, from, handlers, send_size)
    local self = {}
    setmetatable(self, class)
    .....
    return self
end

function handle(self) ..... end

如果模組應該運用 module,那麼我們應該要問這樣是否就是類別模組的定義方式。

lua -l 選項

lua -l 命令列選項沒有副作用的話,就不會派上用場。5.1 中的 M 風格模組,-l 至少會在 package.loaded 中建立一個變數,但這需要迂迴取得,而使用 -l 的用意通常是要縮短啟動詮譯器的方式 (畢竟,-e "require....." 可以達成相同的結果)。在 5.2.0-beta 中,-l 會為 M 風格的模組建立一個全域表格使用 lua_pushglobaltable。不過,使用 lua_pushglobaltable 建立的全域變數可能會比想要的更長,而你可能會想要 -e 'FB = "require 'mypackage.foo.bar'" 的效果。請參閱討論 LuaList:2011-11/msg00016.html

如果 -l 用於建立全域變數,應該將其新增到 _G 嗎?還是以某種方式只限於主程式區塊 (例如,-lfoo 的效果會在主區塊的最上方新增 local foo = require 'foo'_ENV = setmetatable({foo = require 'foo'}, {__index = _G}))?後者比較乾淨。

也有建議指出,-l 應該要接受參數 [14][19]

靜態分析

透過「M」表定義的模組,至少沒有後設資料表,即使它們的形式可能會有些許差異, 也可以從第一原則靜態分析 (例如,詞彙變數和表的行為),就像 LuaInspect 所做的 [28]

5.1 的 module 函式語意比較複雜 (副作用和後設資料表行為)。儘管如此,我們仍可以在較高的層級推斷出意思,特別是在遵循慣例的情況下,就像 [LuaDoc] 等工具已經完成的。5.2 的變更和改善 module 函式的建議也有可能影響這個領域。

改善「module」

提出一些讓模組更好的提議,而非將它丟棄的話題在 https://lua-users.dev.org.tw/lists/lua-l/2011-10/threads.html#00481。(待辦事項:張貼最佳建議在此)

其他使用者的意見

不使用模組功能,表示省略 local 關鍵字,很輕易就能汙染全域環境(這很糟糕,這是那篇文章的目的)。所以我們可以透過將環境改成私人環境(可以繼承自 _G),並在其中定義 _M 表格(現在的表格)的方式,來改善模組功能,它將包含模組的公開介面。我也很關注這些問題,並有一個奇妙的方式可以使用模組功能且不會讓全域環境雜亂。就在這裡

package.loaded[...]={}
module(...) -- you might want to add package.seeall

這並不是透過模組存取全域命名空間的解決方案。我們需要修改過的模組功能。希望在 Lua 的下次版本中會出現。

--MildredKiLya

"不使用模組功能,表示省略 local 關鍵字,很輕易就能汙染全域環境(這很糟糕,...)" -- 沒錯,但是不需要的全域存取會在執行前使用 檢測未定義變數 的方法,而且我認為它們是應當修復的錯誤。

使用模組功能的方法如下,儘管這只是為了規避模組功能目前行為的權宜之計

-- mod.lua
local _E = setmetatable({}, {__index=_G})
local _M = {}
package.loaded[...] = _M
module(...)
_E.setfenv(1, _E)
function _M.test()
  return math.sqrt(9)
end
test2 = 1

--modtest.lua
local m = require "mod"
assert(not mod)
assert(m.test() == 3)
assert(not test)
assert(not test2)
assert(not m.print)
print 'done'

$ luac -p -l mod.lua | lua /usr/local/lua-5.1.3/test/globals.lua
setmetatable    1
_G      1
package 3
module  4
test2   9*
math    7
--DavidManura

另請參閱


近期變更 · 偏好設定
編輯 · 歷史記錄
最後編輯於 2012 年 1 月 3 日凌晨 3:05(格林威治)(diff)