Lua的变量声明和作用域问题提问

5
我是Bitfighter的首席开发人员,我们使用Lua作为脚本语言,允许玩家编写自己的定制机器人船。在Lua中,您不需要声明变量,所有变量默认为全局范围,除非另有声明。这会导致一些问题。例如,看下面的代码片段:
loc = bot:getLoc()
items = bot:findItems(ShipType)     -- Find a Ship

minDist = 999999
found = false

for indx, item in ipairs(items) do           
   local d = loc:distSquared(item:getLoc())  

   if(d < minDist) then
      closestItem = item
      minDist = d
   end
end

if(closestItem != nil) then 
   firingAngle = getFiringSolution(closestItem) 
end

在这个代码片段中,如果findItems()方法没有返回候选项,那么closestItem仍然会指向上次找到的船只,而在这段时间里,该船只可能已经被摧毁了。如果该船只已被摧毁,则它不再存在,getFiringSolution()方法将失败。
你发现问题了吗?我的用户也可能没有发现。虽然微妙,但效果却很显著。
一种解决方案是要求声明所有变量,并将所有变量默认为局部变量。虽然这种改变不能完全防止程序员引用不存在的对象,但可以更难以无意间这样做。
有没有办法告诉Lua将所有变量默认为局部变量,或者要求声明变量?我知道一些其他编程语言(比如Perl)有这个选项。
谢谢!
这里有很多好答案,谢谢! 我决定使用稍微修改过的Lua 'strict'模块。这似乎能让我达到想要的效果,并且我会对其进行一些修改,以便更适合我的特定环境。

顺便问一下,你有在if语句中加入额外的大括号的好理由吗? - Alexander Gladysh
在你提出这个问题之前,我本来会回答它们是必需的。但现在我必须回答省略括号看起来很奇怪! - Watusimoto
2
这当然是个人偏好。但重要的是要记住,Lua不是C、Pascal或其他什么语言。Lua就是Lua,要有效地使用它,必须按照它的方式使用,而不是将其视为其他编程语言的替代品。我发现这样的“语法看起来不对”的事情确实有助于将Lua放入我的大脑中与C++和其他我知道的语言区分开来。简而言之,我的观点是:如果用Lua编写,就要用Lua的方式编写! :-) - Alexander Gladysh
1
是的,你提出了一些好观点。Lua不是C语言,但我认为有些C语言的惯例可以使代码更易读(至少对我来说是这样的:-)。我已经克服了以前在行尾加分号的习惯,并且当我使用C++和Lua时,我会尝试让我的格式化风格有所不同。但是在条件子句中使用括号可能还需要一段时间。 - Watusimoto
1
所以Alex,如果我理解正确的话,如果Lua允许多种编写方式,那么不像其他语言形式的方式比起像其他语言形式的方式更应该被优先选择?如果是这样的话,那么我们必须努力编写尽可能对非Lua编码人员不透明的Lua代码。 - Stomp
4个回答

3

没有设置此行为的选项,但是标准安装提供了一个名为“strict”的模块,它通过修改元表来实现这一点。 用法: require 'strict'

有关更详细的信息和其他解决方案,请参见:http://lua-users.org/wiki/DetectingUndefinedVariables,但我推荐使用“strict”。


这似乎是最简单的答案,也是我正在寻找的。我想更好地理解这里提出的其他解决方案,看看使用“strict”是否有任何缺点。 - Watusimoto

2

有点像。

在Lua中,全局变量概念上存在于全局表_G中(实际情况稍微复杂一些,但从Lua方面来看无法告知)。与所有其他Lua表一样,可以将__newindex元表附加到_G上,以控制如何向其中添加变量。让这个__newindex处理程序在创建全局变量时执行任何你想要的操作:抛出错误、允许但打印警告等。

为了干涉_G,最简单和最清晰的方法是使用setfenv。请参阅文档


2

"默认情况下使用局部变量是错误的"。请参见:

http://lua-users.org/wiki/LocalByDefault

http://lua-users.org/wiki/LuaScopingDiscussion

您需要使用某种全局环境保护。有一些静态工具可以实现这一点(不太成熟),但最常见的解决方案是使用基于_G元表中的__index__newindex的运行时保护。

无耻地插入广告:这个页面也可能会有用:

http://code.google.com/p/lua-alchemy/wiki/LuaGlobalEnvironmentProtection

请注意,虽然它讨论了嵌入到swf中的Lua,但所描述的技术(请参见源代码)适用于通用的Lua。我们在工作中的生产代码中使用类似这些的东西。


1
默认情况下,本地化(可以说)是错误的,但全局化同样如此!我查看了你指出的代码,看起来它做的事情非常类似于上面提到的使用“strict”模块。它们之间有明显的区别吗? - Watusimoto
默认情况下全局变量是不好的,但它是Lua的一项传统遗留问题,几乎无法改变(同时在Lua中编写小型DSL非常有用)。 - Alexander Gladysh
这些严格模块之间的主要区别在于,我的模块试图强制显式和简明地声明全局变量(因为我认为它们是需要在代码中摆脱的邪恶东西,包括全局函数)。Lua 的 etc/strict.lua 模块则稍微不太显式。但这只是个人偏好。 - Alexander Gladysh

0

实际上,具有对飞船过时引用的额外全局变量足以防止GC丢弃对象。因此,可以通过注意到飞船现在已经“死亡”并拒绝对其执行任何操作来在运行时检测它。它仍然不是正确的飞船,但至少不会崩溃。

你可以做的一件事是将用户脚本保存在sandbox中,可能是每个脚本一个沙盒。通过正确操作沙盒的环境表或元表,您可以在调用用户代码之前(或之后)安排丢弃所有或大多数全局变量。

在调用后清理沙盒的好处是丢弃不应该存在的额外引用。这可以通过保留允许在环境中保留的字段的白名单并删除所有其他字段来完成。

例如,以下实现了对用户提供的函数的沙盒调用,其中包含仅在每次调用时提供的新刮擦表后面的白名单名称的环境。

-- 可供用户脚本使用的全局表 local user_G = { print=_G.print, math=_G.math, -- ... } -- 用户沙盒的元表 local env_mt = { __index=user_G }
-- 在一个环境中调用函数,其中可以创建和修改新全局变量,但当用户代码完成时它们将被丢弃。 function doUserCode(user_code, ...) local env = setmetatable({}, env_mt) -- 创建一个带有 RO 全局表的新用户环境 setfenv(user_code, env) -- 将其挂在用户代码上 local results = {pcall(user_code, ...)} setfenv(user_code,{}) return unpack(results) end

如果需要,可以通过再次推回一层元表让全局表只读。

请注意,完整的沙盒解决方案还应考虑对于意外(或恶意)执行无限(或仅仅是非常长的)循环或其他操作的用户代码该怎么办。这个问题通常会在 Lua list 上讨论,但是好的解决方案很难找到。


引用已经失效的船只对象的问题在于,对象的生命周期由主C++程序控制,因此对象可以在没有Lua同意或控制的情况下被删除。Lua具有对userdata指针的引用,该指针突然指向一个无效的对象。我该如何检测到这一点?我需要更好地掌握这个问题,并愿意听取任何建议。我正在沿着沙盒的方向工作,但我确实希望允许创建全局变量,只要它们是有意和故意创建的。以上所有建议似乎都能实现这一点。 - Watusimoto

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