bound scalar globals one

lua-users home
wiki

這個問題:定義一個全域變數,存取並設定它的值實際上會呼叫一個操作內部物件的 C 函式。這在 Lua 4 裡很簡單,但 Lua 5 缺少大多數用於天真的 Lua 4 實作的 metatable。

這裡有一個解決方法,需要以名稱方式繫結全域變數,請參閱 BoundScalarGlobalsTwo 來取得不同的實作。

這裡的想法很簡單:bound scalars 不存在於全域表格中,但 getter 和 setter 函式放在另外兩個表格中,而且這些函式是由全域表格的 __index 和 __newindex 呼叫的。這對具有值的全域變數不會造成效能影響,而且很希望對繫結變數的影響相當有限。(代理可能是更好的名稱,短語「bound scalar」來自 Perl。[1])

這裡,我謹慎地使用現有的來自現有全域表格的 __index 和 __newindex metatable。這些其他現有 metamethods 最好不重要,但它們也能被處理。

-- BindScalar1
-- Version 0.2
-- RiciLake
--
-- Change history
-- --------------
-- Fixed the case where the globals table didn't have a metatable
-- Corrected the behaviour on set where there was no existing
--   __newindex metamethod so that it now rawsets the table
-- Check to see if the old __index method is not a function
--   to mimic the default behaviour
-- Wrote a couple of quick example getters and setters
-- Actually made sure it compiles and runs
--
-- TODO
-- ----
-- Actually debug it with real metatables
-- Think of a setter that lets you set something
--
-- BUGS
-- ----
-- If you specify a getter and don't specify a setter, the binding stops
-- working. It should be necessary to specify both.
--
-- The API needs to be improved

do
  local meta, getters, setters = {}, {}, {}
  local old_meta = getmetatable(getfenv())
  local old_index, old_newindex
  if old_meta then
    old_index, old_newindex = old_meta.__index, old_meta.__newindex
  end
  
  -- at this point you have to populate the getters and setters table
  -- somehow, probably by getting them from your C code.
  -- Here is an example without C:

  -- the getter receives the name of the global as an argument

  local function get_time(k)
    if k == "gmt"
      then return os.date("!%c")
      else return os.date("%c")
    end
  end

  -- the setter actually receives the name and the proposed value
  -- but in this example we don't need them.

  local function set_time()
    error "You cannot change the time"
  end

  -- now put them into getters and setters. There should probably
  -- be a function to do that, something like:
  --   bind_scalar("now", get_time, set_time)

  getters.now = get_time
  getters.gmt = get_time
  setters.now = set_time
  setters.gmt = set_time

  -- Another example. Particular environment variables are made
  -- into globals. (Change this to USERNAME for Windows NT.)

  local function get_env(k)
    return os.getenv(k)
  end

  local function set_env(k, v)
    if os.setenv
      then
        os.setenv(k, v)
      else
        error "You cannot change environment variables on this platform."
    end
  end

  getters.USER = get_env
  setters.USER = set_env -- hmm? it's just an example

  -- It might be nice to change the calls below to object calls,
  -- such as getters[k](getters[k], k)
  -- For efficiency, you probably only want to do that lookup once.

  -- Here is the actual implementation of the metamethods.

  meta = {}

  if type(old_index) == "function" then
      function meta.__index(t, k)
        if getters[k]
          then return getters[k](k)
          else return old_index(t, k)
        end
      end
    elseif type(old_index) == "nil" then
      function meta.__index(t, k)
        if getters[k] then return getters[k](k) end
      end
    else
      function meta.__index(t, k)
        if getters[k]
          then return getters[k](k)
          else return old_index[k]
        end
      end
  end

  if old_newindex
    then
      function meta.__newindex(t, k, v)
        if setters[k]
          then setters[k](k, v)
          else old_newindex(t, k, v)
        end
      end
    else
      function meta.__newindex(t, k, v)
        if setters[k]
          then
            setters[k](k, v)
          else
            rawset(t, k, v)
          end
      end
  end

  setmetatable(getfenv(), meta)
end

範例輸出

-- now is deferred to a function.
> print(now)
Thu Jan 16 12:34:40 2003
-- so is gmt
> print(gmt)
Thu Jan 16 17:35:34 2003
-- setting "works"; the variable is read-only
> now = "tomorrow"
glue.lua:27: You cannot change the time
stack traceback:
        [C]: in function `error'
        glue.lua:27: in function `?'
        glue.lua:91: in function <glue.lua:88>
        stdin:1: in main chunk
        [C]:[C]

-- This mechanism might be useful in a CGI script, for example
> print(USER)
rlake
-- Most platforms implement setenv but it's not ANSI standard. This
-- would work if you patched the os library.
> USER="root"
glue.lua:44: You cannot change environment variables on this platform.
stack traceback:
        [C]: in function `error'
        glue.lua:44: in function `?'
        glue.lua:91: in function <glue.lua:88>
        stdin:1: in main chunk
        [C]:[C]
-- Ordinary globals continue to be ordinary.
> print(j)
nil
> j = 7
> print(j)
7

腳註

[1] 在 Perl 中,這些正式名稱為 [tied 變數](另請參閱 [培養的 Perl:tied 變數]),定義為繫結的一種形式。您將變數的身分識別(名稱或位址)繫結到類別中定義的特定實作。這個類別可被視為其他類別的代理,但可針對繫結代理到語言中的識別碼這件事來說點什麼,這可能需要特別的語言支援。請參閱 [維基百科 Binding_(computer_science)](特別是名稱繫結),以及 [維基百科 Proxy Pattern]。並請參閱在 [維基百科 Free variables and bound variables] 中這類術語。特別是,Perl 的 TIEHASH 和 TIEARRAY 有點類比於 Lua 的元表格機制。Perl 的 TIESCALAR 在 Lua 中似乎沒有這樣的語法,但在本頁面中描述的類似的效果,是修改全域環境的元表格(除非開發出一些像上面給出的通用架構來防止衝突,不然這可能會是不利之處)。--DavidManura

這只有一個問題:__newindex 只在未曾設定的值上呼叫,例如 x = "bla"; x = "more bla" 將只呼叫 __newindex 一次。 -- 匿名人士

是的,這就是它實際上沒有設定值,如果它使用 setter 函式的話。但當您建立 setter 函式時,您必須先確保對應的全域變數沒有設定;如果您想要之後加入 setter 函式,則適用同樣的方式。試試看吧 :) --RiciLake


最近變更 · 喜好設定
編輯 · 歷史
上次編輯 2007年3月22日 下午11:52 GMT (差異)