物件導向教學

lua-users home
wiki

Lua 其實並不是一個物件導向程式語言,它並沒有內建類別(class)的概念。不過,我們可以使用由表格和元表格所組成的自訂類別系統。

簡單的元表格類別

local MyClass = {} -- the table representing the class, which will double as the metatable for the instances
MyClass.__index = MyClass -- failed table lookups on the instances should fallback to the class table, to get methods

-- syntax equivalent to "MyClass.new = function..."
function MyClass.new(init)
  local self = setmetatable({}, MyClass)
  self.value = init
  return self
end

function MyClass.set_value(self, newval)
  self.value = newval
end

function MyClass.get_value(self)
  return self.value
end

local i = MyClass.new(5)
-- tbl:name(arg) is a shortcut for tbl.name(tbl, arg), except tbl is evaluated only once
print(i:get_value()) --> 5
i:set_value(6)
print(i:get_value()) --> 6

首先,我們建立一個表格來表示類別,並包含它的方法。我們也可以同時把它作為實例的元表格使用,不過如果你喜歡,也可以使用獨立的實例元表格。

在建構式中,我們建立實例(一個空的表格),賦予它元表格,填入欄位並傳回新的實例。

在方法中,我們使用「self」參數來取得要操作的實例。這種用法相當常見,因此 Lua 提供了 : 的語法糖,它可以從表格中呼叫函式項目,並在第一個參數之前插入表格本身。

我們可以進行一些改進

local MyClass = {}
MyClass.__index = MyClass

setmetatable(MyClass, {
  __call = function (cls, ...)
    return cls.new(...)
  end,
})

function MyClass.new(init)
  local self = setmetatable({}, MyClass)
  self.value = init
  return self
end

-- the : syntax here causes a "self" arg to be implicitly added before any other args
function MyClass:set_value(newval)
  self.value = newval
end

function MyClass:get_value()
  return self.value
end

local instance = MyClass(5)
-- do stuff with instance...

我們在類別表格中加入一個包含 __call 元方法的元表格,這個元方法會在一個值被當成函式呼叫時觸發。我們讓它呼叫類別的建構式,因此你在建立實例時不需要使用 .new。另一個選擇是直接在元方法中放入建構式。在元方法中,"cls" 會指向目前的表格。

此外,為了搭配 : 方法呼叫捷徑,Lua 讓你可以在表格中定義函式時使用 :,它會隱式地加入 self 參數,讓你不用自己輸入。

繼承

我們可以輕鬆地擴充上述範例中類別的設計,使用繼承

local BaseClass = {}
BaseClass.__index = BaseClass

setmetatable(BaseClass, {
  __call = function (cls, ...)
    local self = setmetatable({}, cls)
    self:_init(...)
    return self
  end,
})

function BaseClass:_init(init)
  self.value = init
end

function BaseClass:set_value(newval)
  self.value = newval
end

function BaseClass:get_value()
  return self.value
end

---

local DerivedClass = {}
DerivedClass.__index = DerivedClass

setmetatable(DerivedClass, {
  __index = BaseClass, -- this is what makes the inheritance work
  __call = function (cls, ...)
    local self = setmetatable({}, cls)
    self:_init(...)
    return self
  end,
})

function DerivedClass:_init(init1, init2)
  BaseClass._init(self, init1) -- call the base class constructor
  self.value2 = init2
end

function DerivedClass:get_value()
  return self.value + self.value2
end

local i = DerivedClass(1, 2)
print(i:get_value()) --> 3
i:set_value(3)
print(i:get_value()) --> 5

我們有一個衍生類別表格和一個 __index 元方法,讓它可以繼承基底類別。另外,我們把實例的建立移到 __call 元方法中,並把建構式變成純粹的初始化方法。這樣一來,衍生類別可以在自身呼叫基底類別的初始化函式。

最後一個可用來進行優化的步驟是將基底類別的內容複製到衍生類別中,而不是使用 __index。這樣可以避免 __index 長長的鏈條,這可能會讓方法呼叫變慢,另外,如果基底類別有像 __add 這類的方法,它們也會像衍生類別上的適當元方法一樣運作。這是因為在尋找元方法時,並不會遵循 __index

local DerivedClass = {}
for k, v in pairs(BaseClass) do
  DerivedClass[k] = v
end
DerivedClass.__index = DerivedClass

類別建立函式

知道了這些之後,我們就可以建立一個便利函式來建立類別,它可以選擇從其他類別繼承。以下是一個這樣的函式範例

function (...)
  -- "cls" is the new class
  local cls, bases = {}, {...}
  -- copy base class contents into the new class
  for i, base in ipairs(bases) do
    for k, v in pairs(base) do
      cls[k] = v
    end
  end
  -- set the class's __index, and start filling an "is_a" table that contains this class and all of its bases
  -- so you can do an "instance of" check using my_instance.is_a[MyClass]
  cls.__index, cls.is_a = cls, {[cls] = true}
  for i, base in ipairs(bases) do
    for c in pairs(base.is_a) do
      cls.is_a[c] = true
    end
    cls.is_a[base] = true
  end
  -- the class's __call metamethod
  setmetatable(cls, {__call = function (c, ...)
    local instance = setmetatable({}, c)
    -- run the init method if it's there
    local init = instance._init
    if init then init(instance, ...) end
    return instance
  end})
  -- return the new class table, that's ready to fill with methods
  return cls
end

基於閉包的物件

我們也可以使用閉包來製作物件。實例建立的速度較慢,而且使用較多的記憶體,不過它也有一些優點(例如更快的實例欄位存取),而且是閉包如何應用的有趣範例。

local function MyClass(init)
  -- the new instance
  local self = {
    -- public fields go in the instance table
    public_field = 0
  }

  -- private fields are implemented using locals
  -- they are faster than table access, and are truly private, so the code that uses your class can't get them
  local private_field = init

  function self.foo()
    return self.public_field + private_field
  end

  function self.bar()
    private_field = private_field + 1
  end

  -- return the instance
  return self
end

local i = MyClass(5)
print(i.foo()) --> 5
i.public_field = 3
i.bar()
print(i.foo()) --> 9

請注意,. 語法用於呼叫方法,而非 :。這是因為 self 變數已經儲存在方法中,作為上值,因此呼叫它的程式碼不需要傳遞它。

也可以以這種方式繼承

local function BaseClass(init)
  local self = {}

  local private_field = init

  function self.foo()
    return private_field
  end

  function self.bar()
    private_field = private_field + 1
  end

  -- return the instance
  return self
end

local function DerivedClass(init, init2)
  local self = BaseClass(init)

  self.public_field = init2

  -- this is independent from the base class's private field that has the same name
  local private_field = init2

  -- save the base version of foo for use in the derived version
  local base_foo = self.foo
  function self.foo()
    return private_field + self.public_field + base_foo()
  end

  -- return the instance
  return self
end

local i = DerivedClass(1, 2)
print(i.foo()) --> 5
i.bar()
print(i.foo()) --> 6

基於表格與基於封閉的類別

基於表格的優點

基於封閉的優點

另請參閱


RecentChanges · 喜好設定
編輯 · 歷史記錄
最後編輯時間為 2015 年 5 月 19 日凌晨 4:47(格林威治標準時間)(diff)