你不想知道的关于Lua的多重价值的一切

2020-05-06 00:18:17

我最近花了很多时间编写茴香,并为它的编译器做贡献。由于它是一种编译Tolua的语言,这也意味着需要花费大量时间来了解Lua。本文将集中讨论Lua(不过我会在脚注中不时提到茴香)。

Lua是一种整洁、优雅、相对简单的语言。我发现它特别值得注意的是它可以嵌入到其他程序中;能够承载多种不同风格的编程;以及它的一个实现LuaJIT的出色性能。在很多方面,它给人的感觉更像是一个更优雅、更有节制的JavaScript--它支持尾部调用消除[1]和协程,它没有令人困惑的var/let区分(尽管它在Lua5.4之前没有像const这样的特性),并且它没有将类语法优先于其他风格的编程(面向对象的编程可以在Lua中使用元表进行,但是没有像在JS中那样给出特殊的语法)。

这就是说,它有一些无疑是古怪的方面-数组和字典被统一到表中,其整数索引从1开始;标准库非常小,这是对它的适用性的认可;它像JavaScript一样,使得变量在默认情况下是全局的,只有在您要求的时候才是局部的,这是一个巨大的错误。奇怪的是,它还支持一种非常不常见的语言特性:多个返回值(为简洁起见,我们将其称为“倍数”)。这是我们今天要详细讨论的最后一个话题。

由于Lua是一种动态语言(例如,与Go相反),因此以一种相当奇怪的方式实现了多个urn。这并没有像我希望的那样在Lua的文档中进行详细的探讨,所以我想提供一个一站式资源,解释我所能想到的关于该语言这一特殊部分的所有内容。

剧透:不幸的是,在您关心的代码中使用乘数将会变得平淡无奇。它们使用起来很笨拙,有很多问题,会带来维护风险,而且(据我所知)它们的性能并不比它们的主要替代产品--表好多少。这不是一篇非常有用的文章,除非您需要了解Lua行为的细节。

在Lua中有三种主要方式来表示多值。其中每一个都有自己独特的用途。下面的每个代码片段都会生成相同的多值:

多值文字:1、2、3。(请注意,这不包括像{1,2,3}这样的可编辑文字)。

函数中的vararg,.:局部函数x(.)。回来..。结束x(1,2,3)

本地函数Two_Values()return";First";,";Second";End local tab={Two_Values()}print(tab[1],tab[2])--print";first Second";

局部函数multival_first(x,.)。RETURN x END PRINT(MULVAL_FIRST(1,2,3))--PRINTS";1";LOCAL函数MULVAL_REST(x,.)。回来..。End Print(multival_rest(1,2,3))--打印";2 3";Print(SELECT(2,1,2,3))--打印";2 3";与最后一行相同。

为了从多值的末尾获得值,我们有两个选项。第一个是递归:

局部函数INCRYMER_EACH_VALUE(FIRST,.)。如果FIRST,则返回FIRST+1,INCREMER_EACH_VALUE(.)。End End Print(INCREMER_EACH_VALUE(1,2,3))--打印";2 3 4";

第二种方法是将多个值打包到一个表中。有两种方法可以做到这一点,下面演示了这两种方法:

--1.可以将Multival直接打包到表文本函数pack_multival(.)。return{.}end--Lua对表的默认打印仅显示标识;我们将改为--打印长度。print(#pack_multival(1,2,3))--prints";3";--2.您可以使用table.pack,它内置于Lua 5.2和更高版本的local tab=table.pack(1,2,3)print(#tab)--prints";3";--table.pack还会在它返回的表上设置";n";字段。这比使用#tab更有效,后者在表中迭代。打印(选项卡。n)--打印";3";

当您开始使用多个数时,它们可能看起来非常简单。只要您正在使用具有固定长度的List或者可以通过遍历列表的头部和尾部来表示的内容,那么使用多值似乎应该是不错的和可预测的。

如果您尝试使用多值包,您可能会遇到的第一个问题来自于期望它们以与其余Luados相同的方式工作。简而言之,Lua的局部变量使用所谓的词典作用域。这意味着变量定义的作用域是您在源代码文件中看到的包含变量定义的代码块。其他函数以什么顺序调用,或者它们是否被调用都无关紧要-局部变量的定义只需查看代码结构即可确定。这与动态作用域相反,在动态作用域中,在给定点定义的变量依赖于程序本身的运行时。Lua的全局变量有效地使用了动态作用域。

如上所述,引用多值只有三种方式:为其返回值调用函数、按字面意思插入用逗号分隔的多值或使用vararg符号.。作为函数参数。我们感兴趣的是最后一个案子。

当你使用..。在函数签名中,必须放在参数列表的末尾。(这就是阻止您-至少在语法上-访问.的末尾的原因。而无需将整个内容赋给单个变量、递归遍历它或将其打包到表中。)。

你也不能把整个东西赋给一个变量,因为赋值.。对于一个变量意味着解包多值并将第一个值赋给变量-如果指定了多个值,则将其赋给多个值。例如:

--将vararg赋给单个变量会将该变量--赋给vararg的_first value_。局部函数try_to_assign_vararg(.)。本地x=.。返回x结束打印(try_to_assign_vararg(1,2,3))--打印";1";

然而,关于vararg还有一条额外的规则,这与Lua的其余部分的行为非常不同。您只能在定义vararg的函数中使用vararg。例如,这样可以防止您将vararg保存到闭包中:

无法运行,并出现以下错误:无法在靠近';.';的vararg函数外部使用';.';。这意味着,当你可以使用..。在两个嵌套函数中,每个函数.。只能引用当前运行的函数的varargof。你不能在闭包中捕获.来持久化它。实际上,..。不是字典作用域的,而是一个动态作用域的变量,具有某些额外的规则,比如不能重新赋值,并且不能在定义它的函数外部使用。

另一种说法是,Lua的乘数是“二等”值。您可能熟悉有第二类函数的语言,第二类函数不能赋值给变量,也不能作为参数传递给函数。这与此类似,但值得注意的例外是,我们可以将vararg传递给另一个函数。我们不能将其保存在变量中,不能将其保存在闭包中,也不能以特定方式对其进行操作。

将乘数传递给函数并将其打包到表中非常简单,但是关于这一点有一个我们尚未克服的主要问题。请看以下示例:

局部函数Returns_Three_Values()return 1,2,3 End Print(Returns_Three_Values())--Prints";1 2 3";print(#{Returns_Three_Values()})--Prints";3";Print(Returns_Three_Values(),4,5)--Prints";1 4 5";Print(#{Returns_Three_Values(),4 5})--Prints";3和#34;

正如第二对打印表达式所演示的那样,如果函数调用是多值中的最后一个,则它只能将多个值返回到多值中。在多变量中使用任何其他值(即使为空)调用函数后,将在第一项截断其返回值。

Lua较好的属性之一是在表中无法区分设置为nil的键和不存在的键。例如,在Javascript中,您既可以使用obj.x=unfinded将属性设置为unfinded,也可以使用delete obj.x将其从对象中实际删除。(还有obj.x=null,但我们暂时忽略它。)在Lua中,只有tab.x=nil,无法区分设置为nil的属性和从未设置的属性。

local y={nil,nil,nil}print(#y)--在表格末尾打印";0";-nils与本地函数multival_length(.)无关。选择(";#";,.)。结束打印(multival_length(table.unpack(Y)--打印";0";print(multival_length(nil,nil,nil))--打印";3";。什么鬼东西?

如本例所示,倍增操作会将该良好属性抛出窗口。我只想说清楚:

在Lua中,multival_length(nil,nil)和multival_length()有一个区别。这种区别是通过SELECT(";#";,.)进行的。

与返回零值的函数(而不是nil)结合使用时,这会变得更加令人困惑:

Print(SELECT(";#";,Print(";a";),Print(";b";),Print(";c";)--首先将";a";、";b";和";c";打印到各自的行上,然后打印";2";。等等,什么?

正如您所预期的那样,返回零值的函数会创建一个空多值。如果您在末尾调用它,它也不会在多项式的末尾添加任何东西。但是,如果对零返回值函数的调用出现在多值结束之前,则会导致在多值中插入nil来代替其零返回值。因此,Lua没有将由所有三个print调用组成的multival折叠成单个零长度的multival,而是折叠了最后一个print调用,但将另外两个调用变为空。这并不适用于表,因为与乘数不同的是,表中的NILS实际上等同于缺少值。

乍一看,这一切似乎都不属于Lua。不带SELECT(";#";,.)。形式,则无法从多值本身的末尾判断多值末尾为零。然而,这使得某些抽象更容易创建。

一种值得注意的情况是选择(";#";,.)。在包装返回乘值的函数时变得特别有用。选择(";#";,.)。让您轻松判断是否可以简单地保存函数的第一个返回值,或者是否需要创建atable来保存所有值。

当第一次查看Lua中的乘数时,决心以某种方式使用它们的程序员可能首先倾向于将它们与尾递归结合起来,以灵活地表示不同的函数。例如,我们可以实现如下范围:

局部函数range1(acc,n)如果n<;1,则返回ELELSE如果n==1,则返回acc,否则返回acc,range1(acc+1,n-1)end end local function range(N)如果n<;1,则error(";n必须为>;=1";)end return range1(1,n)end print(range(3))--prints";1 23";print(#{range(10)})--打印";10";

这个很管用!当我们想要一个长度为n的多值时,我们可以调用range(N),当我们想要等效表时,可以用像{range(N)}这样的大括号将它括起来。这应该可以让我们避免在不需要的时候分配额外的表。由于Lua具有尾部调用消除功能,因此对自身进行的递归调用范围1永远不会打乱堆栈。对吗?

事实证明,多个人打破了尾部呼叫消除。如果您重新返回多个值,Lua需要收集所有这些值,然后才能实际开始返回它们。这意味着它不能丢弃递归函数的堆栈框架。这与在range1内返回{range1(acc,n)}时发生的情况非常相似。那个案子显然不是尾巴。正是乘数的特殊语法使这一点令人困惑,因为它看起来并不像是被任何东西“包围”的人造尾部调用。[2]。

为了避免堆栈溢出,正确地递归函数,我们可以使用下表来实现Range:

本地函数范围1(tab,acc,n)如果n<;1,则返回tab否则tab[acc]=acc返回范围1(tab,acc+1,n-1)END END本地函数范围(N)如果n<;1,则错误(";n必须是&>;=1";)END返回范围1({},1,n)END PRINT(#RANGE(1000x1000))--PRINTS";1000000";

它可能不那么优雅,混合了递归和突变,但是它的运行时特性要好得多。

递归到太多可能会导致堆栈失效,这是有道理的。不太直观的是,即使在没有递归的情况下,乘法运算的大小也有一个上限。请考虑使用我们上面定义的范围函数:

太棒了!现在,让我们试着将其解包成一个多重数。不涉及递归,所以这应该可以很好地工作,对吗?

事实证明,乘法只是对它们可以包含的值的数量有一个上限。没有办法解决这个问题--如果您需要处理一个项目列表而不用担心它有多大,请使用atable,而不是multival。

print((function()return 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42、43、44、45、46、47、48、49、50、51、52、53、54、55、56、57、58、59、60、61、62、63、64、65、66、67、68、69、70、71、72、73、74、75、76、77、78、79、80、81、82、83、84、85。86、87、88、89、90、91、92、93、94、95、96、97、98、99、100、101、102、103、104、105、106、107、108、109、110、111、112、113、114、115、116、117、118、119、120、121、122、123、124、125、126、127、128、129。130、131、132、133、134、135、136、137、138、139、140、141、142、143、144、145、146、147、148、149、150、151、152、153、154、155、156、157、158、159、160、161、162、163、164、165、166、167、168、169、170、171、172、173。174、175、176、177、178、179、180、181、182、183、184、185、186、187、188、189、190、191、192、193、194、195、196、197、198、199、200、201、202、203、204、205、206、207、208、209、210、211、212、213、214、215、216、217。218、219、220、221、222、223、224、225、226、227、228、229、230、231、232、233、234、235、236、237、238、239、240、241、242、243、244、245、246、247、248、249、250、251、252、253、254、255())-投掷";函数或表达式需要太多的寄存器";错误打印(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44、45、46、47、48、49、50、51、52、53、54、55、56、57、58、59、60、61、62、63、64、65、66、67、68、69、70、71、72、73、74、75、76、77、78、79、80、81、82、83、84、85、86、87。88、89、90、91、92、93、94、95、96、97、98、99、100、101、102、103、104、105、106、107、108、109、110、111、112、113、114、115、116、117、118、119、120、121、122、123、124、125、126、127、128、129、130、131。132、133、134、135、136、137、138、139、140、141、142、143、144、145、146、147、148、149、150、151、152、153、154、155、156、157、158、159、160、161、162、163、164、165、166、167、168、169、170、171、172、173、174、175。176、177、178、179、180、181、182、183、184、185、186、187、188、189、190、191、192、193、194、195、196、197、198、199、200、201、202、203、204、205、206、207、208、209、210、211、212、213、214、215、216、217、218、219。220、221、222、223、224、225、226、227、228、229、230、231、232、233、234、235、236、237、238、239、240、241、242、243、244、245、246、247、248、249、250、251、252、253、254、255)--抛出";函数或表达式需要太多寄存器";错误。

但是,对表字面值没有限制,这强化了这样一个事实,即表字面值的内容不是多位数,尽管它们在外观上很相似:

所有这些多值怪癖的不同例子表明,乘法只是另一种数据结构。(我发现这种情况最好的原因是,在多值内递归时,尾部调用消除不起作用,就像它一样

不能将乘数赋给变量。它们只能作为文字、函数调用表达式或vararg.引用。

vararg不能在创建它的函数外部使用(包括在该函数中创建的闭包内)。

在结束之前将乘数插入到另一个多值中时,会在第一个值处截断乘数。

与表格不同,乘数的内置长度与多值内NIL的排列无关。可以使用SELECT(";#";,.)搜索此长度。

当多值在结束前包含对零返回值函数的调用时,函数的返回值将插入nil。

当函数在Multival内进行递归调用时,不会应用尾部调用消除。因此,在业余时间内递归太多次会使堆栈失效。

尝试使用过多参数调用函数或从函数返回将导致错误。这一限制远远低于前面提到的解包表格的限制,仅略低于255个项目。

最后,关于多值,还有一个令我难以置信的主观问题,那就是vararg使用的语法。在我看来,事实上…是有效的Lua使得在示例代码中插入易于理解的占位符变得不必要地困难。

虽然乘数仍然是Lua的一个奇怪而锋利的角落,但我希望多亏了这篇文章,他们会更容易理解。虽然它们很少是特定问题的最佳解决方案,但对于Lua用户来说,了解它们的优点和局限性仍然是有帮助的,即使这只是为了证明避免它们是合理的。

也称为尾部呼叫优化。使用InSafari的JavaScriptCore有适当的Tail CallSupport,但大多数JS开发人员无法利用这一点,因为它在大多数平台上的流行浏览器中不可用。↩。

在茴香中,这种句法混乱会变得更糟!在那里,多个数没有自己的特殊语法,而是用特殊的值表示。(值1 2 3)在Fennelis中与在Lua中的1、2、3相同。不幸的是,这使得进入此场景的递归函数看起来更像真正的尾递归函数。这是在Fennel中使用多个函数和递归时需要注意的问题!↩