Lua:何时可以使用冒号语法?

7

虽然我理解了基本的.:之间的差异, 但我还没有完全弄清楚Lua允许何时使用冒号语法。例如,像这样的东西确实可以工作:

s = "test"
-- type(s) is string.
-- so I can write a colon function for that type
function string:myFunc() 
  return #self 
end

-- and colon function calls are possible
s:myFunc()

然而,同样的模式似乎并不适用于其他类型。例如,当我使用一个table而不是string时:
t = {}
-- type(t) is table.
-- so I can write a colon function for that type
function table:myFunc() 
  return #self 
end

-- Surprisingly, a colon function call is not not possible!
t:myFunc() -- error: attempt to call method 'myFunc' (a nil value)
-- But the verbose dot call works
table.myFunc(t)

转到另一种类型:
x = 1
-- type(x) is number.
-- So I was expecting that I can write a colon function 
-- for that type as well. However, in this case even this
-- fails: 
function number:myFunc() 
  return self 
end
-- error: attempt to index global 'number' (a nil value)

我目前正在努力理解这个问题。结论是正确的吗?

  • 某些类型,如string,允许冒号函数定义和冒号函数调用。
  • 其他类型,如table,仅允许冒号函数定义而不允许冒号函数调用。
  • 还有一些类型,如number,两者都不允许。

这些差异的原因是什么?是否有一个显示所有类型及其支持的冒号语法类型的列表?在number情况下,是否有可能绕过限制,例如编写x:abs()

3个回答

7
string的第一个例子有效,因为所有字符串都共享同一个元表,并且它存储在名为string的表中。
string中可以看到:
引用:

字符串库将其所有函数提供在string表内。 它还为字符串设置了元表,其中__index字段指向string表。 因此,您可以以面向对象的方式使用字符串函数。例如,string.byte(s,i)可以编写为s:byte(i)


table的第二个例子无效,因为每个表都有自己的元表,名为table的表只是table库所有函数的集合。
默认情况下,像数字这样的类型不支持元表。

1
那么 string 是唯一允许这样做的类型吗? - bluenote10
冒号语法只是一种语法糖。例如,表支持冒号语法,但并没有一个通用的元表适用于所有表。 - Yu Hao
1
@bluenote10,文件句柄是另一个例子。 - lhf

6
作为一个全新的Lua用户,我花了一些时间才理解@Yu Hao的回答,所以我会尝试为其他初学者添加一些细节。如果有任何错误,请纠正我。
就我所知,像x:someFunc()这样的调用在以下情况下有效:
  • x有一个元表
  • 元表有一个字段__index
  • 该字段指向一个包含函数someFunc的表。
正如Yu Hao所指出的,字符串自动获得一个指向表的元表,例如:
th> s = 'test'

th> getmetatable(s)
{
  __mod : function: 0x40c3cd30
  __index : 
    {
      upper : function: builtin#82
      rep : function: builtin#79
      split : function: 0x40ffe888
      gfind : function: builtin#87
      find : function: builtin#84
      reverse : function: builtin#80
      lower : function: builtin#81
      len : function: 0x40af0b30
      tosymbol : function: 0x40ffe8a8
      myFunc : function: 0x41d82be0 -- note: this comes from our custom function string:myFunc()
      dump : function: builtin#83
      byte : function: builtin#76
      char : function: builtin#77
      gmatch : function: builtin#87
      match : function: builtin#85
      sub : function: builtin#78
      gsub : function: builtin#88
      format : function: builtin#89
    }
}

因此,在这种情况下,s:myFunc()会自动起作用。为了在table中使用冒号语法,我们可以手动设置它的元表:

th> function enableColonForTable(t)
..>   meta = {__index = table}
..>   setmetatable(t, meta)
..> end

th> t = {}

th> enableColonForTable(t)

th> t:insert(1) -- works now!

另一个观察结果是,__index 指向的表与类型名称完全相同并不重要。我们不仅可以使用 meta = {__index = table},还可以这样做:
th> arbitraryScope = {}

th> function arbitraryScope:test() return "something" end

th> t = {}

th> setmetatable(t, {__index = arbitraryScope})
{}

th> t:test()
something   

这也是与“数字”案例的关键区别。虽然有名为“字符串”和“表格”的现有表,但没有名为“数字”的现有表。这就是为什么以前甚至定义例如“function number:abs()”也失败的原因。但我们仍然可以使其起作用。
th> number = {}

th> function number:abs() return math.abs(self) end

th> x = -123

th> debug.setmetatable(x, {__index = number})
-123    

th> x:abs()
123 

请注意,这里必须使用debug.setmetatable而不是setmetatable。两者之间的区别似乎在于setmetatable仅为给定实例设置元表,而debug.setmetatable则为整个类型设置元表。显然,为数字设置单独的元表是被禁止的(而且也没有太多意义)。这意味着(与表相比),新构造的数字现在默认情况下具有给定的元表,因此以下代码可以正常工作:
th> y = -42

th> y:abs()
42  

[*] 更新

正如Tom Blodget所指出的那样,如果x本身充当命名空间,即它是一个带有方法字段someFunc的表,则x:someFunc()也可以工作。例如,您可以执行table:insert(1)。但现在,命名空间(名为table的表)将作为self传递,您将向命名空间添加数据:

th> print(getmetatable(table)) -- note: "table" does not have a metatable
nil 

th> table:insert(1) -- yet a colon syntax call works

th> table
{
  prune : function: 0x4156bde0
  getn : function: 0x41eb0720
  maxn : function: builtin#90
  remove : function: 0x41eb08c8
  foreachi : function: 0x41eb05b8
  sort : function: builtin#93
  concat : function: builtin#92
  unpack : function: builtin#16
  splice : function: 0x4156bdc0
  foreach : function: 0x41eb0688
  1 : 1
  pack : function: builtin#94
  insert : function: builtin#91
}

重述得不错,但你忽略了一个显而易见的事实:表格字段可以被称为方法。请参阅我在答案中提供的背景信息。 - Tom Blodget

4

补充回答:

首先,需要注意函数是一个值(也称为"一等公民")。

:是Lua中三个索引运算符之一。索引运算符从可索引的对象中返回“字段”的值,可以是函数或任何其他类型。

索引运算符按通用性顺序排列:

  1. 表达式[表达式2]
  2. 表达式.标识符
  3. 表达式:标识符(参数列表)

后两个只是“语法糖”,可以以任何上面的形式重写。

如果“expression2”始终是相同的字符串,并且它是Lua标识符,则使用第二个。

如果针对“expression”索引“identifier”返回的值始终是您要使用“expression”作为隐式第一个参数调用的函数,则使用第三个。这样的函数调用称为“方法调用”。

请注意,语言/编译器不关心/知道字段值是否为函数(如果尝试调用不是函数的值,则会在运行时出错)。它也不关心/知道函数是否为方法(如果不传递适当的参数,该函数可能不会按您预期的方式执行)。
现在,表达式值的类型必须是可以索引的任何类型。请注意,表达式没有编译时类型,因此如果表达式值的类型无法索引,则会发生运行时错误。可索引类型包括:表和具有__index元方法的任何对象。其他答案提供了这些细节。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接