简单 Lua 类

lua-users home
wiki

一种简化的 Lua 类声明方式

Lua 本身没有类系统,但它强大的元编程功能使得定义经典对象变得直截了当。事实上,有很多方法可以做到这一点;再加上不熟悉的符号,这使得 Lua 中的面向对象编程一开始看起来有点吓人。

这里描述的方法是最常见和最灵活的方法,使用元表。可以通过给一个表赋予一个元表来定制它的行为。例如,如果元表有一个 __index 函数,那么任何在表中查找东西的失败尝试都会传递给 __index。如果 __index 本身是一个表,则将在该表中查找符号。(请参阅 PiL 中的精彩讨论 [1])以下是基本思路

Account = {}
Account.__index = Account

function Account:create(balance)
   local acnt = {}             -- our new object
   setmetatable(acnt,Account)  -- make Account handle lookup
   acnt.balance = balance      -- initialize our object
   return acnt
end

function Account:withdraw(amount)
   self.balance = self.balance - amount
end

-- create and use an Account
acc = Account:create(1000)
acc:withdraw(100)

这里,Account 对象由表表示,这些表只包含一个字段,即余额。Lua 尝试在 acc 中查找 withdraw,但找不到。因为 acc 有一个定义了 __index 的元表,它将随后在该元表中查找 withdraw。所以 acc:withdraw(100) 实际上是调用 Account.withdraw(acc,100)。我们实际上可以将 withdraw 直接放入 acc 中,但这将是浪费和不灵活的 - 添加一个新方法将需要更改 create 函数等。

一种简化的创建类的方式

我将定义一个函数 class,它将透明地完成所有这些(以及更多)。

Account = class(function(acc,balance)
              acc.balance = balance
           end)

function Account:withdraw(amount)
   self.balance = self.balance - amount
end

-- can create an Account using call notation!
acc = Account(1000)
acc:withdraw(100)

在这个方案中,你为新类提供一个初始化函数,并自动生成一个“构造函数”。

支持简单的继承。例如,这里定义了一个基类 Animal,并声明了几种特定类型的动物。所有使用 class 创建的类都有一个 is_a 方法,你可以使用它在运行时找出实际的类

-- animal.lua

require 'class'

Animal = class(function(a,name)
   a.name = name
end)

function Animal:__tostring()
  return self.name..': '..self:speak()
end

Dog = class(Animal)

function Dog:speak()
  return 'bark'
end

Cat = class(Animal, function(c,name,breed)
         Animal.init(c,name)  -- must init base!
         c.breed = breed
      end)

function Cat:speak()
  return 'meow'
end

Lion = class(Cat)

function Lion:speak()
  return 'roar'
end

fido = Dog('Fido')
felix = Cat('Felix','Tabby')
leo = Lion('Leo','African')

D:\Downloads\func>lua -i animal.lua
> = fido,felix,leo
Fido: bark      Felix: meow     Leo: roar
> = leo:is_a(Animal)
true
> = leo:is_a(Dog)
false
> = leo:is_a(Cat)
true

Animal 类只定义了 __tostring 方法,Lua 会在需要对象字符串表示时使用它。而 __tostring 方法依赖于 speak 方法,但 speak 方法并没有定义。所以,Animal 类相当于 C++ 中的抽象基类;像 Dog 这样的具体派生类则定义了 speak 方法。请注意,如果派生类有自己的初始化函数,它们必须显式地调用基类的 init 函数。

class() 函数的实现

class() 函数使用两个技巧。它允许你使用调用符号(如上面的 Dog('fido'))来构造一个类,方法是给类本身一个元表,该元表定义了 __call 方法。它通过复制基类的字段到派生类来处理继承。这不是实现继承的唯一方法;我们可以将 __index 设置为一个函数,该函数显式地尝试在基类中查找函数。但这种方法会带来更好的性能,代价是使类对象稍微臃肿一些。每个派生类都保留一个 _base 字段,该字段包含基类,但这是为了实现 is_a 方法。

注意,在运行时修改基类不会影响其子类。

-- class.lua
-- Compatible with Lua 5.1 (not 5.0).
function class(base, init)
   local c = {}    -- a new class instance
   if not init and type(base) == 'function' then
      init = base
      base = nil
   elseif type(base) == 'table' then
    -- our new class is a shallow copy of the base class!
      for i,v in pairs(base) do
         c[i] = v
      end
      c._base = base
   end
   -- the class will be the metatable for all its objects,
   -- and they will look up their methods in it.
   c.__index = c

   -- expose a constructor which can be called by <classname>(<args>)
   local mt = {}
   mt.__call = function(class_tbl, ...)
   local obj = {}
   setmetatable(obj,c)
   if init then
      init(obj,...)
   else 
      -- make sure that any stuff from the base class is initialized!
      if base and base.init then
      base.init(obj, ...)
      end
   end
   return obj
   end
   c.init = init
   c.is_a = function(self, klass)
      local m = getmetatable(self)
      while m do 
         if m == klass then return true end
         m = m._base
      end
      return false
   end
   setmetatable(c, mt)
   return c
end

注释

如果进行此更改

--- class_orig.lua      2009-07-24 20:53:25.218750000 -0400
+++ class.lua   2009-07-24 20:53:49.734375000 -0400
@@ -21,8 +21,8 @@
   mt.__call = function(class_tbl,...)
     local obj = {}
     setmetatable(obj,c)
-    if ctor then
-       ctor(obj,...)
+    if class_tbl.init then
+       class_tbl.init(obj,...)
     else
     -- make sure that any stuff from the base class is initialized!
        if base and base.init then

那么我们可以用这种方式声明类

A = class()
function A:init(x)
  self.x = x
end
function A:test()
  print(self.x)
end

B = class(A)
function B:init(x,y)
  A.init(self,x)
  self.y = y
end

顺便说一句,你可能注意到 class.lua 也适用于运算符

function A:__add(b)
  return A(self.x + b.x)
end

"init" 可以改名为 "__init",因为它是一个私有函数,类似于 __add,并且类似于 Python 的 __init__ [2].

--DavidManura

在原始版本中,名为 'ctor' 的参数实际上是 init 方法,如(之前)语句所示
c.init = ctor
. 我将此参数的名称更改为 'init'。--DeniSpir

一个可选的类构建器(在 lua 5.3 中测试)--ThaBubble
function class(def)
    local class = {}
    local parents = {}
    
    local upv
    local env = _G
    
    local wraps
    local function super(parent_class)
        if not parent_class then
            parent_class = parents[1]
        end
        
        local this = this
        local that = {}
        for k,v in pairs(parent_class) do
            that[k] = type(v) == 'function' and wraps(this, v) or v
        end
        
        return setmetatable(that, that)
    end
    
    function wraps(this, func)
        return function(...)
                local t = env.this
                local s = env.super
                
                env.this  = this
                env.super = super
                
                local ret  = pcall(func, ...)

                env.this  = t
                env.super = s
                
                return ret
            end
    end
    
    function class.__init()end

    for i=1,math.huge do
        inherit, v = debug.getlocal(def, i)
        if not inherit then break end
        
        local parent_class = _G[inherit]
        for i=1,math.huge do
            local  name, pclass = debug.getlocal(2,i,1)
            if not name then break
            elseif name == inherit then
                parent_class  = pclass
                break
            end
        end

        if parent_class  and type(parent_class) == 'table' then
            table.insert(parents, parent_class)
            for k,v in pairs(parent_class) do
                class[k] = v
            end
        else
            error(string.format('Class "%s" not valid.', name))
        end
    end
    
    
    for i=1,math.huge do
        local  name, value = debug.getupvalue(def, i)
        if not name then break
        elseif name == '_ENV' then
            env = value
            upv = i
            break
        end
    end

    local _env = setmetatable({}, {
        __index= function(t, name)
            local  value  = class[name]
            return value ~= nil and value or env[name]
        end,
        __newindex = function(t, name, value)
                class[name] = value
            end
    })
    
    local function senv(env)
        if upv then debug.setupvalue(def, upv, env)
        else _G = env end
    end
    
    senv(_env)
    env.pcall(def, env.table.unpack(parents))
    senv(env)
    
    return setmetatable({}, {
        __ipairs    = function()        return ipairs(class)              end,
        __pairs     = function()        return  pairs(class)              end,
        __index     = function(t, name) return        class[name]         end,
        __index_new = function(t, name, value)        class[name] = value end,
        __call      = function(...)
            local this = {}
            for k,v in pairs(class) do
                this[k] = type(v) == 'function' and wraps(this, v) or v
            end
            this.__class = class
            this.__init(...)
            
            return setmetatable(this, this)
        end
    })
end

示例
global  = true
Inherit = class(function()
    this_is_a_property_of_Inherit = true
    
    function __init()
        print('Inherit().__init()')
        this.init = true
    end
    
    function __call()
        print('Yay! You\'re calling for me :) init:', this.init, '\n')
    end
end)

Example = class(function(Inherit)
    print('Inherited property:', this_is_a_property_of_Inherit)
    print('Global variable:   ', global, '\n')
    
    function __init()
        print('Example().__init()')
        super().__init()
        print('this.init:', this.init)
    end
    
    function test(...)
        print(..., this.__init, '\n')
    end
end)

example = Example()
example.test('__init:')
example()

example.property = 'I\'m a property of instance "example"'
print('example.property', example.property)
print('Example.property', Example.property)

--    Inherited property:	true
--    Global variable:   	true	

--    Example().__init()
--    Inherit().__init()
--    this.init:	true
--    __init:	function: 0x15dd5f0	

--    Yay! You're calling for me :) init:	true	

--    example.property	I'm a property of instance "example"
--    Example.property	nil

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2017 年 6 月 6 日下午 7:22 GMT (差异)