首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Basic Concepts

本节介绍该语言的基本概念。

2.1 – Values and Types

Lua是一种动态类型的语言。这意味着变量没有类型; 只有值才有。该语言中没有类型定义。所有的值都有自己的类型。

Lua中的所有值都是第一类对象。这意味着所有值都可以存储在变量中,作为参数传递给其他函数,并作为结果返回。

Lua有八种基本类型:nilbooleannumberstringfunctionuserdatathreadtable。类型nil有一个单一的值,nil,其主要属性是不同于任何其他值; 它通常代表没有有用的价值。布尔类型有两个值,falsetrue。nil和false为假; 任何其他值都是真。类型number代表整数和实数(浮点数)。类型string表示不可变的字节序列。Lua是8位清理:字符串可以包含任何8位值,包括嵌入的零(' \0')。Lua也是编码不可知的; 它不会假定字符串的内容。

类型编号使用两个内部表示,或两个子类型,一个称为整数,另一个称为浮点。Lua明确规定了每个表示的使用时间,但它也根据需要自动进行转换(请参阅第3.4.3节)。因此,程序员可能会选择忽略整数和浮点数之间的差异,或者完全控制每个数字的表示。标准Lua使用64位整数和双精度(64位)浮点数,但您也可以编译Lua,以便使用32位整数和/或单精度浮点数(32位)。对于小型机器和嵌入式系统,整数和浮点数均为32位的选项特别具有吸引力。(请参阅LUA_32BITS文件中的宏luaconf.h。)

Lua可以调用(和操作)用Lua编写的函数和用C编写的函数(参见§3.4.10)。两者都由类型函数表示。

提供类型userdata以允许将任意C数据存储在Lua变量中。用户数据值表示一块原始内存。有两种用户数据:完整用户数据,它是由Lua管理的一块内存的对象,以及轻量级用户数据,它只是一个C指针值。用户数据在Lua中没有预定义的操作,除了赋值和身份测试。通过使用metatables,程序员可以定义完整的用户数据值的操作(请参阅§2.4)。用户数据值不能在Lua中创建或修改,只能通过C API。这保证了主机程序拥有的数据的完整性。

类型线程表示独立的执行线程,用于实现协程(请参阅第2.6节)。Lua线程与操作系统线程无关。Lua支持所有系统上的协同程序,甚至是那些原生不支持线程的协程。

类型表实现了关联数组,也就是说,不仅可以用数字对数组进行索引,而且还可以使用除nil和NaN 之外的任何Lua值。(不是数字是用于表示未定义或不可表示的数字结果的特殊值,例如0/0。)表可以是异构的 ; 也就是说,它们可以包含所有类型的值(nil除外)。任何值为零的键都不被视为表格的一部分。相反,不属于表的任何键都有相关的值nil

表是Lua中唯一的数据结构化机制; 它们可以用来表示普通数组,列表,符号表,集合,记录,图形,树等。为了表示记录,Lua使用字段名称作为索引。该语言通过提供a.name作为语法糖来支持这种表示a["name"]。在Lua中创建表格有几种方便的方法(参见§3.4.9)。

与索引一样,表字段的值可以是任何类型。特别是,因为函数是第一类值,所以表字段可以包含函数。因此表格也可以携带方法(见§3.4.11)。

表格的索引遵循语言中原始平等的定义。表达式a[i]a[j]表示同一个表元素当且仅当ij原始相等(即没有元方法时相等)。具体而言,具有整数值的浮点数等于它们各自的整数(例如,1.0 == 1)。为了避免歧义,任何用作键的积分值的浮点数都被转换为相应的整数。例如,如果您编写a[2.0] = true,插入表中的实际密钥将是整数2。(另一方面,2和“ 2”是不同的Lua值,因此表示不同的表项。)

表,函数,线程和(完整)用户数据值都是对象:变量实际上不包含这些值,只包含对它们的引用。赋值,参数传递和函数返回总是处理对这些值的引用; 这些操作并不意味着任何形式的副本。

库函数type返回一个描述给定值类型的字符串(请参阅第6.1节)。

2.2 – Environments and the Global Environment

正如将在§3.2和§3.3.3中讨论的那样,任何对自由名称的引用(也就是说,没有绑定到任何声明的名称)都会var被语法转换为_ENV.var。而且,每个块都被编译在一个名为外部局部变量_ENV(请参阅§3.3.2)的范围内,所以_ENV它本身永远不会是块中的空闲名称。

尽管存在这个外部_ENV变量和自由名称的翻译,但它_ENV是一个完全正规的名称。特别是,您可以使用该名称定义新的变量和参数。每个对自由名称的引用都_ENV遵循Lua的通常可见性规则(参见§3.5),使用该程序中当前可见的内容。

任何用作值的表_ENV称为环境。

Lua拥有一个称为全球环境的杰出环境。该值保存在C注册表中的特殊索引处(请参阅第4.5节)。在Lua中,全局变量_G用这个相同的值进行初始化。(_G从未在内部使用。)

当Lua加载一个块时,其_ENV最大值的默认值是全局环境(请参阅参考资料load)。因此,默认情况下,Lua代码中的自由名称是指全局环境中的条目(因此它们也被称为全局变量)。而且,所有标准库都在全球环境中加载,并且在那里有一些功能在该环境中运行。您可以使用load(或loadfile)加载具有不同环境的块。(在C中,您必须加载块,然后更改其第一个upvalue的值。)

2.3 – Error Handling

由于Lua是一种嵌入式扩展语言,所有Lua动作都从主机程序中的C代码开始,从Lua库中调用一个函数。(当你使用Lua standalone时,lua应用程序就是宿主程序。)当编译或执行Lua块时发生错误时,控制权返回给主机,主机可以采取适当的措施(如打印错误信息)。

通过调用error函数,Lua代码可以明确地产生一个错误。如果您需要在Lua中发现错误,则可以使用pcallxpcall保护模式下调用给定的函数。

每当出现错误时,错误对象(也称为错误消息)将传播有关错误的信息。Lua本身只会生成其错误对象是字符串的错误,但程序可能会将错误对象的任何值生成错误。由Lua程序或其主机来处理这些错误对象。

当你使用xpcall或者时lua_pcall,你可能会给一个消息处理程序以在发生错误时被调用。该函数与原始错误对象一起调用并返回一个新的错误对象。在错误展开堆栈之前调用它,以便它可以收集有关错误的更多信息,例如通过检查堆栈并创建堆栈回溯。该消息处理程序仍受受保护呼叫的保护; 所以,消息处理程序中的错误将再次调用消息处理程序。如果这个循环持续太久,Lua会打破它并返回一个合适的消息。(消息处理程序仅为常规运行时错误而调用,不会在调用终结器时调用内存分配错误或错误。)

2.4 – Metatables and Metamethods

Lua中的每个值都可以有一个metatable。这个metatable是一个普通的Lua表,它定义了特定操作下原始值的行为。您可以通过在其metatable中设置特定字段来更改操作行为的某些方面。例如,当一个非数字值是一个加法的操作数时,Lua会__add在值的metatable 的字段“ ”中检查一个函数。如果它找到一个,Lua调用这个函数来执行加法。

metatable中每个事件的关键字是一个事件名称前面加上两个下划线的字符串; 相应的值称为metamethods。在前面的例子中,键是“ __add”,metamethod是执行加法的函数。

您可以使用该getmetatable函数查询任何值的metatable 。Lua使用原始访问查询metatables中的元方法(请参阅参考资料rawget)。因此,为了检索ev对象中的事件的metamethod o,Lua会执行与以下代码等效的操作:

代码语言:javascript
复制
rawget(getmetatable(o) or {}, "__ev")

您可以使用该setmetatable函数替换表格的元表。你不能从Lua代码中改变其他类型的metatable(除了使用调试库(§6.10)); 你应该使用C API。

表和完整的userdata具有单独的metatables(尽管多个表和userdata可以共享它们的metatables)。所有其他类型的值每种类型共享一个单一的metatable; 也就是说,对于所有数字都有一个单一的metatable,对于所有的字符串都有一个metatable,默认情况下,一个值没有metatable,但是字符串库为字符串类型设置了一个metatable(见第6.4节)。

metatable控制对象在算术运算,按位运算,顺序比较,连接,长度操作,调用和索引中的表现。metatable也可以定义一个函数,当用户数据或表被垃圾收集时(§2.5)被调用。

对于一元运算符(否定,长度和位非),元方法计算并用一个等于第一个操作数的虚拟第二操作数调用。这个额外的操作数仅仅是为了简化Lua的内部操作(通过使这些操作符像二进制操作一样)并且在将来的版本中可能会被删除。(对于大多数用途,这个额外的操作数是不相关的。)

下面给出了由metatables控制的事件的详细列表。每个操作都由其相应的密钥标识。

  • __add:加法(+)操作。如果添加的操作数不是一个数字(也不是一个数字的强制字符串),Lua会尝试调用一个metamethod。首先,Lua会检查第一个操作数(即使它是有效的)。如果该操作数没有定义metamethod __add,那么Lua将检查第二个操作数。如果Lua可以找到一个元方法,它将以两个操作数作为参数来调用元方法,并且调用的结果(调整为一个值)是操作的结果。否则,它会引发错误。
  • __sub:减法(-)操作。与添加操作类似的行为。
  • __mul:multiplication(*)操作。与添加操作类似的行为。
  • __div:division(/)操作。与添加操作类似的行为。
  • __mod:模(%)操作。与添加操作类似的行为。
  • __pow:指数运算(^)。与添加操作类似的行为。
  • __unm:否定(一元-)操作。与添加操作类似的行为。
  • __idiv:地板分区(//)操作。与添加操作类似的行为。
  • __band:按位AND(&)操作。类似于加法操作的行为,除非Lua会尝试元方法,如果任何操作数既不是整数也不是强制为整数的值(请参阅第3.4.3节)。
  • __bor:按位OR(|)操作。类似于按位AND操作的行为。
  • __bxor:按位异或(二进制~)操作。类似于按位AND操作的行为。
  • __bnot:按位NOT(一元~)操作。类似于按位AND操作的行为。
  • __shl:按位左移(<<)操作。类似于按位AND操作的行为。
  • __shr:按位右移(>>)操作。类似于按位AND操作的行为。
  • __concat:concatenation(..)操作。类似于加法操作的行为,除非Lua会尝试一个元方法,如果任何操作数既不是字符串也不是数字(它总是对字符串进行强制处理)。
  • __len:length(#)操作。如果对象不是字符串,Lua会尝试它的元方法。如果有一个metamethod,Lua将该对象作为参数进行调用,并且调用的结果(始终调整为一个值)是操作的结果。如果没有元方法,但对象是一个表,那么Lua将使用表长度操作(请参阅第3.4.7节)。否则,Lua会产生一个错误。
  • __eq:equal(==)操作。行为类似于加法操作,除非Lua只会在比较的值都是表或全部用户数据并且它们不是基本相等时才尝试metamethod。调用的结果总是转换为布尔值。
  • __lt:小于(<)操作。类似于加法操作的行为,除非Lua仅在比较的值既不是数字也不是两个字符串时才尝试metamethod。调用的结果总是转换为布尔值。
  • __le:不等于(<=)的操作。与其他操作不同,不太平等的操作可以使用两个不同的事件。首先,Lua __le在两个操作数中寻找metamethod,就像在少于操作中一样。如果它找不到这样的metamethod,那么它会尝试__ltmetamethod,假设它a <= b相当于not (b < a)。与其他比较运算符一样,结果始终是布尔值。(这个__lt事件的使用可以在将来的版本中被删除;它也比真实的__le元方法慢)。
  • __index:索引访问table[key]。这个事件发生在table不是表格或者key不存在时table。metamethod被抬头table。尽管名称,这个事件的metamethod可以是一个函数或表。如果它是一个函数,它被称为与tablekey作为参数,并且该呼叫(调整到一个值)的结果是该操作的结果。如果它是一个表格,最终结果就是将此表格编入索引的结果key。(这个索引是规则的,不是原始的,因此可以触发另一个元方法。)
  • __newindex:索引分配table[key] = value。和索引事件一样,这个事件发生在table不是表格或者key不存在时table。metamethod被抬头table。与索引一样,此事件的元方法可以是函数也可以是表格。如果它是一个功能,它被称为用tablekey以及value作为参数。如果它是一个表,Lua会使用相同的键和值对此表执行索引分配。(这个任务是规则的,不是原始的,因此可以触发另一个元方法。)每当有一个__newindex元方法时,Lua就不执行原语分配。(如有必要,metamethod本身可以打电话rawset来完成作业。)
  • __call:通话操作func(args)。当Lua尝试调用非函数值(即,func不是函数)时,会发生此事件。metamethod被抬头func。如果存在,metamethod被称为func第一个参数,然后是原始调用(args)的参数。调用的所有结果都是操作的结果。(这是允许多个结果的唯一元方法。)

在将其设置为某个对象的metatable之前,将所有需要的metamethods添加到表中是一种很好的做法。特别是,__gcmetamethod只有在遵循这个顺序时才起作用(见§2.5.1)。

由于metatables是常规表,因此它们可以包含任意字段,而不仅仅是上面定义的事件名称。标准库中的一些功能(例如tostring)使用metatables中的其他字段来实现自己的目的。

2.5 – Garbage Collection

Lua执行自动内存管理。这意味着您不必担心为新对象分配内存或在不再需要对象时释放内存。Lua通过运行垃圾回收器来自动管理内存以收集所有死对象(即,不再可从Lua访问的对象)。Lua使用的所有内存都受自动管理:字符串,表格,用户数据,函数,线程,内部结构等。

Lua实现了一个增量式标记和扫描收集器。它使用两个数字来控制其垃圾收集周期:垃圾收集器暂停垃圾收集器步骤乘数。两者均以百分点为单位(例如,值100表示​​内部值为1)。

垃圾收集器暂停控制收集器在开始新循环之前等待的时间。较大的值使收集器不那么积极。小于100的值意味着收集器不会等待开始新的循环。值为200意味着收集器在开始新周期之前等待使用的总内存翻倍。

垃圾收集器步骤乘法器控制收集器相对于内存分配的相对速度。较大的值使收集器更具侵略性,但也会增加每个增量步骤的大小。您不应该使用小于100的值,因为它们会使收集器太慢,并可能导致收集器永远不会完成一个循环。默认值是200,这意味着收集器以内存分配速度的“两倍”运行。

如果您将步骤乘数设置为非常大的数字(大于程序可以使用的最大字节数的10%),则收集器的行为就像是一个停止世界的收集器。如果您将暂停设置为200,那么收集器的行为与旧版本的Lua版本相同,每当Lua将其内存使用量翻倍时,会执行完整的收集。

您可以通过lua_gc在C或collectgarbageLua中调用来更改这些数字。您也可以使用这些功能直接控制收集器(例如,停止并重新启动它)。

2.5.1 – Garbage-Collection Metamethods

您可以为表格设置垃圾收集器元方法,并使用C API为完整的用户数据设置(请参阅第2.4节)。这些metamethods也被称为finalizer。终结器允许您将Lua的垃圾回收与外部资源管理(如关闭文件,网络或数据库连接,或释放自己的内存)进行协调。

对于要收集的对象(表或用户数据)进行最终确定时,您必须标记为最终确定。在设置其元数据时,您将对象标记为最终化,并且metatable具有由字符串“ __gc” 索引的字段。请注意,如果您在没有__gc字段的情况下设置了metatable ,然后在metatable中创建该字段,则该对象将不会标记为最终化。

当一个标记的对象变成垃圾时,垃圾收集器不会立即收集它。相反,Lua把它列入清单。收集完成后,Lua会浏览该列表。对于列表中的每个对象,它检查对象的__gc元方法:如果它是一个函数,则Lua将该对象作为其单个参数进行调用; 如果metamethod不是函数,那么Lua会忽略它。

在每个垃圾收集周期结束时,以相反的顺序调用对象的终结器,以便在该周期中收集的对象被标记为最终确定; 也就是说,要被调用的第一个终结器是与程序中最后标记的对象相关联的终结器。每个终结器的执行可能会在执行常规代码的任何时候发生。

因为所收集的对象必须仍然被终结使用,该对象(并且只能通过其可访问的其他对象),必须复活由Lua中。通常,这种复活是暂时的,并且在下一次垃圾收集循环中释放对象内存。但是,如果终结器将对象存储在某个全局位置(例如全局变量)中,则复活是永久的。此外,如果终结器再次将终结对象标记为终结对象,则在下一个对象无法访问的周期中将再次调用终结器。在任何情况下,只有在对象不可访问且未标记为完成的GC循环中才释放对象内存。

当你关闭一个状态时(参见lua_close),Lua调用所有标记为最终化的对象的终结器,按照它们被标记的相反顺序。如果任何终结器在该阶段标记用于收集的对象,则这些标记不起作用。

2.5.2 – Weak Tables

一个弱表是一个表,它的元素是弱引用。垃圾收集器忽略弱引用。换句话说,如果对象的唯一引用是弱引用,那么垃圾收集器将收集该对象。

弱表可以具有弱键,弱值或两者。具有较弱值的表允许收集其值,但会阻止收集其密钥。具有弱键和弱值的表格允许集合键和值。在任何情况下,如果收集密钥或值,则将整个对从表中移除。桌子的弱点__mode由其metatable字段控制。如果该__mode字段是包含字符' k' 的字符串,则表中的键很弱。如果__mode包含“ v',表中的值很弱。

具有弱键和强值的表格也被称为ephemeron表格。在ephemeron表中,只有在密钥可达的情况下才认为该值可访问。特别是,如果只有一个键的引用通过它的值,那么这个键被删除。

只有在下一个收集周期中,表中任何弱点的变化才会生效。特别是,如果您将弱点更改为更强大的模式,则Lua可能会在更改生效之前从该表中收集一些项目。

只有具有显式构造的对象才会从弱表中删除。值(如数字和轻量级C函数)不受垃圾回收处理,因此不会从弱表中删除(除非其相关值被垃圾回收系统收集)。虽然字符串需要垃圾回收,但它们没有明确的构造,因此不会从弱表中删除。

复活对象(即,正在完成的对象和只能通过正在完成的对象才可访问的对象)在弱表中具有特殊行为。在运行终结器之前,它们将从弱值中移除,但是在运行终结器之后,只有在下一个集合中才从弱键中移除它们,而这些对象实际上已被释放。此行为允许终结器通过弱表访问与对象关联的属性。

如果收集周期中的复活对象中存在弱表,那么在下一个周期之前可能无法正确清除。

2.6 – Coroutines

Lua支持协程,也称为协作多线程。Lua中的协程表示一个独立的执行线程。然而,与多线程系统中的线程不同,协程只通过显式调用yield函数来暂停执行。

通过调用创建协程coroutine.create。它的唯一参数是一个函数,它是协程的主要功能。该create函数只创建一个新的协程并返回一个句柄(一个线程类型的对象); 它不启动协程。

你通过调用来执行协程coroutine.resume。当您第一次调用时coroutine.resume,将第一个参数作为返回的线程传递coroutine.create,协程通过调用其主函数来开始执行。传递给它的额外参数coroutine.resume作为参数传递给该函数。协程开始运行后,它会一直运行,直到它终止或退出

协程可以通过两种方式终止它的执行:通常,当主函数返回(显式或隐式地,在最后一条指令之后)时; 并且在异常情况下,如果存在无保护的错误。在正常终止的情况下,coroutine.resume返回,再加上协程主函数返回的任何值。如果有错误,则coroutine.resume返回false加错误对象。

协程通过调用产生收益coroutine.yield。当协程产生时,coroutine.resume即使产量发生在嵌套函数调用(即,不在主函数中,而是在由主函数直接或间接调用的函数中),也立即产生相应的返回。在yield的情况下,coroutine.resume也返回true,加上传递给它的值coroutine.yield。下一次恢复同一个协程时,它会继续执行它的执行点,并调用coroutine.yield返回传递给它的额外参数coroutine.resume

类似coroutine.createcoroutine.wrap功能还创建了一个协程,但不是返回协同程序本身,它返回一个函数,调用它时,恢复协程。传递给这个函数的任何参数都是作为额外的参数coroutine.resumecoroutine.wrap返回由coroutine.resume第一个返回的所有值(布尔错误代码)。不像coroutine.resumecoroutine.wrap不会发现错误; 任何错误都会传播给调用者。

作为协程的工作原理的例子,请考虑以下代码:

代码语言:javascript
复制
function foo (a)
  print("foo", a)
  return coroutine.yield(2*a)
end

co = coroutine.create(function (a,b)
      print("co-body", a, b)
      local r = foo(a+1)
      print("co-body", r)
      local r, s = coroutine.yield(a+b, a-b)
      print("co-body", r, s)
      return b, "end"
end)

print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))

当你运行它时,它会产生以下输出:

代码语言:javascript
复制
co-body 1       10
foo     2
main    true    4
co-body r
main    true    11      -9
co-body x       y
main    true    10      end
main    false   cannot resume dead coroutine

您还可以创建和操作通过C API协同程序:看功能lua_newthreadlua_resumelua_yield

扫码关注腾讯云开发者

领取腾讯云代金券