“self”如何访问本地表的键?

All*_*nzi 2 lua

我是Lua新手,正在学习这个视频。38:56,代码如下:

Sequence = {}
function Sequence:new()
  local new_seq = {last_num = 0}
  self.__index = self
  return setmetatable(new_seq, self)
  end

function Sequence:next()
  print(self.last_num)
  end
Run Code Online (Sandbox Code Playgroud)

我的理解是self相当于Sequence,并且self被设置为 的元表new_seq,并且这个元表__index也是self

并且last_num是表的一键new_seq而不是一键,怎么在函数self的定义中next可以写成是一键呢?self.last_numlast_numself

而且,在调用之前setmetatable,有self.__index = self,我以为只有元表有__index一个特殊的键,但在调用之前 setmetatableSequence只是一个普通表,还不是元表,怎么会有呢__index

nob*_*ody 5

简洁版本:

\n\n
\n

我的理解是self相当于Sequence\xe2\x80\xa6

\n
\n\n

self是由方法样式函数定义产生的隐式参数。在函数内,它将引用作为该函数的第一个参数传入的任何值。

\n\n

(你关于自我的其他问题也源于同样的困惑。)

\n\n
\n

我认为只有元表有__index一个特殊的键\xe2\x80\xa6

\n
\n\n

元表只是一个表。__index只是一个像其他键一样的键,您可以在任何表上定义具有该名称的字段。仅当对表的查找失败并且 Lua 注意到该表具有附加的元表时,元表的名为 \xe2\x80\x93 的字段__index才具有特殊含义,因为 Lua 将在此处查找处理程序。

\n\n

__index包含一个表只是处理程序的另一种特殊情况(因为它很常见),__index = some_other_table大致相当于__index = function( table, key ) return some_other_table[key] end\xe2\x80\x93 ,即“去那里看看some_other_table如果table[key]是空的”。print(如果您无法理解所发生的情况,使用长版本和其中的某些内容可能会有所帮助。)

\n\n
\n\n

长版本,对代码进行脱糖处理并详细介绍:

\n\n

定义与(名称是自动选择的,大致类似于其他语言)function foo:bar( ... )相同。另外,与 相同。这意味着上面的代码与\xe2\x80\xa6相同function foo.bar( self, ... )selfthisfunction foo.bar( ... )foo.bar = function( ... )

\n\n
Sequence = {}\nSequence.new = function( self )\n  local new_seq = { last_num = 0 }\n  self.__index = self\n  return setmetatable( new_seq, self )\nend\n\nSequence.next = function( self )\n  print( self.last_num )\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xe2\x80\xa6 相当于\xe2\x80\xa6

\n\n
Sequence = {\n  new = function( self )\n    local new_seq = { last_num = 0 }\n    self.__index = self\n    return setmetatable( new_seq, self )\n  end,\n  next = function( self )\n    print( self.last_num )\n  end,\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,本质上,它定义的是一个包含两个函数的表,每个函数都有一个参数。这两个函数中的第二个函数非常简单:它只是打印所传递的任何表的字段next内容(使用名称来引用它)。last_numself

\n\n

现在,就像定义一样,有一些:用于调用的语法糖。调用foo:bar( ... )转换为foo.bar( foo, ... ),因此当您有some_sequence并说时some_sequence:next( ),会发生调用some_sequence.next( some_sequence )\xe2\x80\x93:定义的 -syntax 引入了一个额外的隐藏参数,并且:调用的 -syntax 填充了该额外参数。通过这种方式,您将其视为方法的函数可以访问您将其视为对象的表,并且一切都会顺利进行。

\n\n

new函数有点复杂——我将其重写为另一种等效形式,以使其更易于阅读:

\n\n
function Sequence.new( self )\n  self.__index = self\n  return setmetatable( { last_num = 0 }, self )\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,对于传入的任何表,它都会将该表分配给__index同一个表的字段,并返回一个新表,并将该旧表设置为元表。(是的,这件事很令人困惑\xe2\x80\xa6 别担心,继续阅读即可。)要了解其原因和工作方式,这里有一个示例:

\n\n

如果你说some_sequence = Sequence:new( ),你将拥有以下结构:

\n\n
some_sequence = { last_num = 0 } -- metatable:-> Sequence\nSequence = { new = (func...), next = (func...), __index = Sequence }\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,当您说时some_sequence:next( ),这将转化为呼叫some_sequence.next( some_sequence )。但some_sequence没有字段next!因为some_sequence有一个元表,Lua 去查看 \xe2\x80\x93 在本例中,元表是Sequence。当查找(或“索引”)操作“失败”(它会返回nil)时,Lua 在元表的字段中查找处理程序__index,(再次)找到一个表Sequence,然后重新尝试对该表进行查找(找到next我们定义的函数)。

\n\n

这意味着在这种情况下我们可以等效地编写Sequence.next( some_sequence )(但一般来说您不想 \xe2\x80\x93 或无法 \xe2\x80\x93 手动解析这些引用)。如上所述,next只打印它收到的表的字段值last_num- 在本例中它得到some_sequence。再说一次,一切都很顺利。

\n\n
\n\n

更多评论(还有另一个例子):

\n\n

作为介绍性示例,该代码比必要的更令人费解且脆弱。这是另一个版本(并不相同,实际上行为不同,但应该更容易理解):

\n\n
Sequence = { }\nSequence.__index = Sequence\nfunction Sequence.new( )\n    return setmetatable( { last_num = 0 }, Sequence )\nend\nfunction Sequence:next( )\n    print( self.last_num )\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

0当您运行以下命令时,您拥有的版本和此版本都会打印:

\n\n
some_sequence = Sequence:new( )\nsome_sequence:next( )\n
Run Code Online (Sandbox Code Playgroud)\n\n

(上面我已经描述了当您对代码执行此操作时会发生什么,请在继续阅读之前进行比较并尝试找出我的版本会发生什么。)

\n\n

这也将为0两个版本打印:

\n\n
sequences = { [0] = Sequence }\nfor i = 1, 10 do\n    local current = sequences[#sequences]\n    sequences[#sequences+1] = current:new( )\nend\nlocal last = sequences[#sequences]\nlast:next( )\n
Run Code Online (Sandbox Code Playgroud)\n\n

两个版本的底层发生的情况有很大不同。sequences您的代码如下所示:

\n\n
sequences[0] = Sequence -- with __index = Sequence\nsequences[1] = { last_num = 0, __index = sequences[1] } -- metatable:->Sequence\nsequences[2] = { last_num = 0, __index = sequences[2] } -- metatable:->sequences[1]\nsequences[3] = { last_num = 0, __index = sequences[3] } -- metatable:->sequences[2]\n...\n
Run Code Online (Sandbox Code Playgroud)\n\n

这就是我的版本的样子:

\n\n
sequences[0] = Sequence -- __index = Sequence, as set at the start\nsequences[1] = { last_num = 0 } -- metatable:->Sequence\nsequences[2] = { last_num = 0 } -- metatable:->Sequence\nsequences[3] = { last_num = 0 } -- metatable:->Sequence\n...\n
Run Code Online (Sandbox Code Playgroud)\n\n

(如果你sequences[#sequences+1] = Sequence:new( )在上面的循环中说,你的代码也会产生这个。)

\n\n

使用我的版本,调用last:next( )无法 find next,查看元表 ( Sequence),找到一个__index字段(再次,Sequence)并找到next,然后继续按上述方式调用它。

\n\n

在您的版本中,调用last:next( )无法 find next,查看元表 ( sequences[9]),找到一个__index字段 ( sequences[9]),找不到next,因此查看元表( 的sequences[9],即sequences[8]),找到一个__index字段 ( sequences[8]),找不到next,因此查看元表...(直到我们到达sequences[1])...找不到next,查看元表(Sequence),找到一个__index字段(Sequence),最后找到next,然后继续调用。(这就是为什么我说它很难遵循......)

\n\n

您拥有的代码实现了基于原型的 OOP,具有所有优点和缺点。正如您所看到的,查找遍历整个链,这意味着您可以定义一个函数sequences[5].next来执行其他操作,然后sequences[5]通过sequences[10]该函数找到另一个函数。这非常有用 \xe2\x80\x93 不需要所有样板定义一个新类来更改某些功能,只需调整一个对象并将其像类一样使用即可。(如果您不小心这样做,这也会很烦人。)

\n\n

我的版本实现的东西更接近于许多其他语言中基于类的 OOP。(你不能意外地同时重写多个对象的方法。)这两种方法(以及 Lua 中的许多其他 OOP 方法)的共同点是定义与方法同名的对象字段将隐藏该方法并使其无法访问。(如果你定义some_sequence.next,说some_sequence:next( )some_sequence.next( some_sequence )会立即找到next你定义的,Lua将不会费心去查看元表等等。)

\n