Lua, как сделать таблицу классов и создание экземпляров?

Вопрос возник из http://tylerneylon.com/a/learn-lua/ учебника. включает коды:

Dog = {dog1 = 'original dog class'}
function Dog.new(self, ... )
    newObj = {sound = 'woof'}
    self.__index = self
    return setmetatable(newObj, self)
end

function Dog.makeSound(self, ... )
    print('I say' .. self.sound)
end

print('Dog=', Dog)
print('Dog.metatable=', getmetatable(Dog))  -- this will output nothing

myDog = Dog.new(Dog)
print('\nmyDog=', myDog)
print('myDog.metatable=', getmetatable(myDog))
myDog.makeSound(myDog)

Это результат приведенных выше кодов в учебнике:

wirelessprvnat-172-17-106-141:Programming frankhe$ th test2.lua
Dog=  {
  makeSound : function: 0x0a6cec20
  dog1 : "original dog class"
  new : function: 0x0a6cec00
}
Dog.metatable=  nil 

myDog=  {
  sound : "woof"
}
myDog.metatable=  {
  makeSound : function: 0x0a6cec20
  __index : 
    {
      makeSound : function: 0x0a6cec20
      __index : 
        {
          makeSound : function: 0x0a6cec20
          __index : 
            {
              makeSound : function: 0x0a6cec20
              __index : 
                {
                  makeSound : function: 0x0a6cec20
                  __index : {...}
                  dog1 : "original dog class"
                  new : function: 0x0a6cec00
                }
              dog1 : "original dog class"
              new : function: 0x0a6cec00
            }
          dog1 : "original dog class"
          new : function: 0x0a6cec00
        }
      dog1 : "original dog class"
      new : function: 0x0a6cec00
    }
  dog1 : "original dog class"
  new : function: 0x0a6cec00
}
I saywoof

Одна дополнительная фотография для более четкого изображения вопроса

Хотя реализация в учебнике успешно печатает «I saywoof», метатаблица myDog, по-видимому, не так желательна, как мы ожидали. Поэтому мое решение ниже (различия в Dog.new):

function Dog.new(self, ... )
    newObj = {sound = 'woof'}
    return setmetatable(newObj, {__index = self})
end

Результат моего решения:

wirelessprvnat-172-17-106-141:Programming frankhe$ th test2.lua
Dog=  {
  makeSound : function: 0x0d7f2978
  dog1 : "original dog class"
  new : function: 0x0d7f2958
}
Dog.metatable=  nil 

myDog=  {
  sound : "woof"
}
myDog.metatable=  {
  __index : 
    {
      makeSound : function: 0x0d7f2978
      dog1 : "original dog class"
      new : function: 0x0d7f2958
    }
}
I saywoof

Мой код печатает «I saywoof» и имеет более точную структуру таблицы. Я хочу знать, какая реализация правильная, та, что в учебнике, или моя? Кроме того, я хочу знать, почему код в учебнике генерирует итеративное определение метатаблицы Dog.


person Frank He    schedule 09.05.2016    source источник
comment
Какая print функция производит этот вывод? Я не верю, что это правильная иерархия. Я считаю, что ваше изменение просто создает новую таблицу каждый раз, когда вызывается Dog:new(), а не повторное использование существующей таблицы Dog. (Хотя я согласен с тем, что self.__index = self в функции new является странным, поскольку вам действительно нужно сделать это только один раз для каждого класса (а не один раз для каждого экземпляра, поскольку self является классом).   -  person Etan Reisner    schedule 09.05.2016
comment
Я добавил фото, чтобы более точно описать вопрос. Print — это просто функция печати по умолчанию в lua.   -  person Frank He    schedule 09.05.2016
comment
Оба подхода правильны, но неоптимальны. В одном из учебника используется хак для экономии памяти с использованием одной и той же таблицы для методов и метаметодов. См. первую часть этого ответа. Ваш подход выделяет новую метатаблицу для каждого объекта Dog. Вы должны разместить метатаблицу за пределами Dog.new и повторно использовать ее.   -  person siffiejoe    schedule 09.05.2016
comment
Стандартный lua print не печатает такие таблицы. Ах, th, похоже, интерпретатор факела с настраиваемой функцией print. И я считаю, что это просто сбивает с толку таблица, имеющая себя как __index и возвращающаяся к своей максимальной глубине четыре. Если вы вызовете setprintlevel, чтобы изменить это значение, держу пари, вы увидите, что он напечатает ровно столько вложенных уровней, сколько вы установили для печати.   -  person Etan Reisner    schedule 09.05.2016


Ответы (1)


Давайте посмотрим на структуру таблицы Dog, которая после создания объекта Dog имеет метаметод __index, установленный следующим образом:

Dog = {}            --> table: 000000000079a510     
Dog.__index = Dog   --> table: 000000000079a510

Когда вы печатаете таблицу Dog, ключ __index имеет значение содержащей его таблицы, что приводит к рекурсии. Стандартный Lua не печатает таблицы, поэтому эта функция print должна остановиться после ~5 уровней (то есть: __index : {...}, где она останавливает рекурсию). Как упоминал @siffiejoe в комментариях, это метод использования одной таблицы как для методов объекта, так и для метаметодов.

Относительно того, какая реализация является правильной; в Lua есть много способов создавать объекты. Пример класса, хотя и не является неправильным, ИМХО без необходимости использует глобальные переменные. Его реализация просачивается в глобальную среду через Dog и newObj. Не большая проблема по отдельности, но когда это часть более крупной программы, это может быть источником трудно обнаруживаемых ошибок. Альтернативный метод заключается в реализации вашего класса в виде модуля. Используйте локальные переменные для реализации и экспортируйте только то, что необходимо для создания экземпляров новых объектов.

Например, давайте посмотрим на рефакторинг класса Dog:

-- class: dog.lua
--
local Dog = {}     -- the objects implementation
Dog.__index = Dog  -- also, its own metatable

-- instantiate a Dog object:
local function new(name, sound)
    local self = {
        name = name,
        sound = sound or 'woof'
    }
    return setmetatable(self, Dog)
end

-- implement object methods:
function Dog.say(self)
    print(('<%s> says: %s'):format(self.name, self.sound))
end

-- implement object metamethods (unique to Dog objects):
function Dog.__tostring(self)
    return ('Dog: %s'):format(self.name)
end

-- module exports:
return {
    new = new;       -- Dog constructor
    __object = Dog;  -- Dog object table/metatable
}

Модуль экспортирует конструктор, который знает, как построить объект Dog без необходимости в глобальном объекте.

-- original example:
myDog = Dog.new(Dog)  --> must pass in the global Dog table to create new objects

-- vs --

-- refactored example:
local Dog = require 'dog'   --> Dog object factory
local myDog = Dog.new()     --> instantiate new Dog

С наследованием можно справиться, объединив метатаблицы и вызвав конструктор родителей в функции new:

-- class: colorfuldog.lua
--
local Dog = require 'dog'   -- import the parent class

local ColorfulDog = setmetatable({}, Dog.__object)  -- inherit from Dog
ColorfulDog.__index = ColorfulDog                   -- also, its own metatable

-- instantiate a new ColorfulDog object:
local function new(name, sound, color)
    local self = Dog.new(name, sound)  -- construct the parent first
    self.color = color
    return setmetatable(self, ColorfulDog)
end

-- implement or override object methods:
function ColorfulDog.lookat(self)
    print(('<%s> looks: %s'):format(self.name, self.color))
end

-- implement object metamethods (unique to ColorfulDog objects):
function ColorfulDog.__tostring(self)
    return ('ColorfulDog: %s'):format(self.name)
end

-- module exports
return {
    new = new;
    __object = ColorfulDog;
}

Таким образом, каждый класс инкапсулируется в свой собственный модуль, который не пропускает детали реализации в глобальную среду.

-- script: test.lua
--
local Dog = require 'dog'
local ColorfulDog = require 'colorfuldog'

local d1 = Dog.new 'Rover'
local d2 = Dog.new('Max', 'arf!')
local d3 = ColorfulDog.new('Lassie', 'ruff', 'brown')

d1:say()  -- sugar for d1.say(d1)
d2:say()
d3:say()  -- inherited from Dog
d3:lookat()

print(d1, d2, d3) 

Запуск вышеуказанных выходных данных:

$ lua test.lua
<Rover> says: woof
<Max> says: arf!
<Lassie> says: ruff
<Lassie> looks: brown
Dog: Rover      Dog: Max        ColorfulDog: Lassie

Как я уже говорил ранее, существует много много способов создания классов в Lua, это лишь пример одного из них. Как бы вы ни решили реализовать объекты, по-прежнему рекомендуется поддерживать чистоту глобальной среды.

person Adam    schedule 09.05.2016
comment
Привет Адам, большое спасибо за этот очень подробный ответ! У меня есть еще один вопрос о stackoverflow.com/questions/37377830/ Думаю, вы, скорее всего, сможете ответить на мой вопрос. Так что, пожалуйста, посмотрите, когда вы свободны, спасибо! - person Frank He; 22.05.2016
comment
Большое спасибо за этот краткий пример введения в классы и наследование в lua! - person pmckeown; 13.04.2020