0 引言
游戏开发中经常需要使用Lua
语言作为胶水语言,来辅助游戏的快速开发。所以就总结一下学习Lua
的一些心得。
相关参考文章:
游戏开发为什么要使用Lua
Lua是如何与C++进行交互的
1 基础语法
Lua 是一种轻量级的脚本语言,语法简洁且易于学习。以下是 Lua 脚本的基础语法,包括变量、数据类型、控制结构、函数、表、元表等。
1.1 变量和数据类型
Lua 是动态类型语言,变量不需要声明类型。
-- 变量
local a = 10 -- 整数
local b = 3.14 -- 浮点数
local c = "Hello" -- 字符串
local d = true -- 布尔值
local e = nil -- 空值
1.2 注释
Lua 支持单行注释和多行注释。
-- 单行注释
--[[
多行注释
可以跨越多行
]]
1.3 控制结构
条件语句
local x = 10
if x > 0 then
print("x is positive")
elseif x < 0 then
print("x is negative")
else
print("x is zero")
end
循环语句
-- while 循环
local i = 1
while i <= 5 do
print(i)
i = i + 1
end
-- for 循环
for i = 1, 5 do
print(i) -- 输出1,2,3,4,5
end
-- 泛型 for 循环
local t = {10, 20, 30}
for index, value in ipairs(t) do
print(index, value) -- 输出 1,10 2,20 3,30
end
1.4 函数
-- 定义函数
function add(a, b)
return a + b
end
-- 调用函数
local result = add(3, 4)
print(result) -- 输出 7
-- 匿名函数
local multiply = function(a, b)
return a * b
end
print(multiply(3, 4)) -- 输出 12
1.5 表(Table)
表是 Lua 中唯一的数据结构,可以用来表示数组、字典、集合、对象等。同时也可以存放函数
-- 创建一个空表
local t = {}
-- 数组
local array = {1, 2, 3, 4, 5}
print(array[1]) -- 输出 1
-- 字典(又称散列表、哈希表)
local dict = {name = "Alice", age = 30}
print(dict["name"]) -- 输出 "Alice"
print(dict.age) -- 输出 30
-- 嵌套表
local nested = {a = {b = {c = 10}}}
print(nested.a.b.c) -- 输出 10
----------------------------------------------------------------------------------
-- 定义一个表
local myTable = {}
-- 将函数存储在表中
myTable.sayHello = function()
print("Hello, World!")
end
-- 调用表中的函数
myTable.sayHello() -- 输出:Hello, World!
----------------------------------------------------------------------------------
在 Lua 中,往表(table)中插入键值对有多种方式。以下是几种常见的方法:
-- 1. 使用方括号 `[]` 语法
-- 这是最常见和直接的方法。
local myTable = {}
myTable["key1"] = "value1"
myTable["key2"] = "value2"
-- 2. 使用点 `.` 语法
-- 这种方法只能用于键是有效的标识符(即由字母、数字和下划线组成,且不能以数字开头)。
local myTable = {}
myTable.key1 = "value1"
myTable.key2 = "value2"
-- 3. 在表构造时直接初始化
-- 在创建表时,可以直接在构造器中插入键值对。
local myTable = {
key1 = "value1",
key2 = "value2"
}
-- 4. 使用 `table.insert` 函数
-- `table.insert` 通常用于插入数组(即具有连续整数键的表)中的元素,但也可以用于插入键值对。
local myTable = {}
table.insert(myTable, "value1") -- 插入到数组中,键为1
table.insert(myTable, "value2") -- 插入到数组中,键为2
-- 5. 使用 `rawset` 函数
-- `rawset` 函数可以直接设置表的键值对,绕过元表(metatable)的 `__newindex` 元方法。
local myTable = {}
rawset(myTable, "key1", "value1")
rawset(myTable, "key2", "value2")
-- 6. 使用 `setmetatable` 和 `__newindex` 元方法
-- 通过元表和 `__newindex` 元方法,可以自定义插入键值对的行为。
local myTable = {}
local mt = {
__newindex = function(table, key, value)
rawset(table, key, value)
print("Inserted key: " .. key .. ", value: " .. value)
end
}
setmetatable(myTable, mt)
myTable.key1 = "value1" -- 会触发 __newindex 元方法
myTable["key2"] = "value2" -- 也会触发 __newindex 元方法
-- 7. 使用 `for` 循环批量插入
-- 如果有多个键值对需要插入,可以使用 `for` 循环。
local myTable = {}
local keys = {"key1", "key2", "key3"}
local values = {"value1", "value2", "value3"}
for i = 1, #keys do
myTable[keys[i]] = values[i]
end
1.6 字符串操作
Lua 提供了一些常用的字符串操作函数。
local str = "Hello, World!"
-- 获取字符串长度
print(#str) -- 输出 13
-- 字符串连接
local str2 = str .. " Lua"
print(str2) -- 输出 "Hello, World! Lua"
-- 字符串查找
local start, finish = string.find(str, "World")
print(start, finish) -- 输出 8 12
-- 字符串替换
local newStr = string.gsub(str, "World", "Lua")
print(newStr) -- 输出 "Hello, Lua!"
1.7 模块和包
Lua 支持模块和包,可以通过 require
函数加载模块。
-- mymodule.lua
local mymodule = {}
function mymodule.greet(name)
print("Hello, " .. name)
end
return mymodule
-- main.lua
local mymodule = require("mymodule")
mymodule.greet("World") -- 输出 "Hello, World"
2 进阶
2.1 Lua表的存储原理
回答面试官时,解释Lua表的实现原理可以分为以下几个关键点,用通俗易懂的方式表达:
2.1.1 Lua表的本质
Lua表(table)其实是一个“万能容器”,可以同时充当数组(按顺序存储多个元素)和字典(通过键值对存储)。它的实现核心是同时结合了数组部分和哈希部分。
- 数组部分:存放的是带有连续整数索引的数据,比如
{1, 2, 3}
这样的表,它会直接存在数组部分中。 - 哈希部分:存放的是非整数索引或者稀疏的整数索引,比如字符串、表作为键值,或者不连续的整数键值对。哈希表通过计算键的哈希值快速找到对应的值。
2.1.2 双重结构的优势
Lua表之所以使用这两部分结构,是为了兼顾不同类型的操作——像数组的访问是连续的,性能很好,而字典的访问需要通过哈希计算,适合处理非整数的键。因此,Lua会根据键的类型智能地决定数据存放在数组部分还是哈希部分,这样就能保持操作的高效性。
- 举个例子:当你用整数作为索引,并且这些整数是连续的(例如
{1, 2, 3, 4}
),它会把数据放在数组部分,查找和存储都会很快。 - 如果键不是整数或不连续(例如
{key="value"}
),就会放在哈希部分,仍然可以通过哈希函数快速查找。
2.1.3 举例
对于 {1, name = "nihao", 3}
这种表,它既包含了单个值(1
和 3
,也就是整数索引的值),又包含了键值对(name = "nihao"
,这是非整数键的键值对)。在Lua内部的存储中,会将这种混合形式分别存储在数组部分和哈希部分。
存储方式:
-
数组部分:存储具有连续整数索引的值。
- 在这个例子中,
1
和3
分别是整数索引 1 和 2 的值,符合数组部分的存储规则,所以它们会被放在数组部分。
- 在这个例子中,
-
哈希部分:存储非整数键(例如字符串)或不连续的整数索引。
name = "nihao"
是一个字符串键,因此会被存放在哈希部分,因为哈希部分专门处理非整数类型的键。
具体存储结果:
-
数组部分:
[1] = 1, [2] = 3
(按顺序存储的值)。 -
哈希部分:
["name"] = "nihao"
(键是字符串,存入哈希表)。 -
当访问
tbl[1]
或tbl[2]
时,Lua会优先在数组部分查找。 -
当访问
tbl["name"]
时,Lua会在哈希部分查找。
总结:Lua表支持这种混合形式,它会智能地将整数索引的值放入数组部分,将非整数索引的键值对放入哈希部分。这样既能保持查找的高效性,又能灵活存储各种不同类型的键和值。
2.2 元表和元方法
在Lua中,元表(Metatable) 和 元方法(Metamethods) 是非常强大和灵活的机制,它们允许你改变或扩展Lua表的行为。通过元表,你可以实现例如算术运算符重载、对象继承、访问控制等功能。
2.2.1. 元表(Metatable)
元表是Lua中的一种特殊表,它可以为其他Lua表赋予新的行为。你可以把元表看作是表的“幕后操控者”——它可以定义当表执行某些特定操作时(如加法、索引等),应该触发什么行为。
每个Lua表都可以关联一个元表,通过这个元表,你可以为表定义额外的行为,而这些行为并不直接存在于表中,而是通过元表间接实现的。
2.2.2. 元方法(Metamethods)
元方法是定义在元表中的特殊函数,用于控制某些操作的行为。比如,当你对一个表进行加法运算时,Lua会检查该表是否有与之关联的 元表 ,并查找这个 元表 中的特定 元方法 (如 __add
元方法)。如果存在该元方法,Lua就会使用它来处理加法运算。
元方法的名称通常以双下划线开头,比如 __index
、__newindex
、__add
、__tostring
等。
2.2.3. 常见元方法
以下是一些常见的元方法,以及它们对应的作用:
-
__index
:当试图访问表中不存在的键时,会调用这个元方法。它通常用于实现“继承”或“默认值”。例子:
local metatable = { __index = function(table, key) return "键不存在" end } local myTable = {} setmetatable(myTable, metatable) print(myTable.someKey) -- 输出:"键不存在"
-
__newindex
:当试图给表中不存在的键赋值时,会触发这个元方法。它可以用来控制表中数据的写入行为。例子:
local metatable = { __newindex = function(table, key, value) print("不能直接设置新值") end } local myTable = {} setmetatable(myTable, metatable) myTable.someKey = 42 -- 输出:"不能直接设置新值"
-
__add
:控制表的加法运算。你可以通过它为表定义自定义的加法行为。例子:
local metatable = { __add = function(table1, table2) return table1.value + table2.value end } local t1 = {value = 10} local t2 = {value = 20} setmetatable(t1, metatable) setmetatable(t2, metatable) print(t1 + t2) -- 输出:30
-
__tostring
:当试图将表转换为字符串(例如通过print()
输出时)时,会调用这个元方法。它可以用来定义表的自定义字符串表示。例子:
local metatable = { __tostring = function(table) return "这是一个自定义表" end } local myTable = {} setmetatable(myTable, metatable) print(myTable) -- 输出:"这是一个自定义表"
2.2.4. 如何设置元表
你可以通过 setmetatable()
函数为一个表设置元表:
local myTable = {} -- 创建一个普通表
local metatable = {} -- 创建元表
setmetatable(myTable, metatable) -- 设置 myTable 的元表为 metatable
如果你想获取表的元表,可以使用 getmetatable()
函数:
local mt = getmetatable(myTable) -- 获取 myTable 的元表
2.2.5. 使用 __index
实现表继承
Lua没有内置的类和继承机制,但通过元表的 __index
元方法,你可以模拟类的继承行为。
例子:
local base = {value = 10}
local derived = {}
setmetatable(derived, {__index = base})
print(derived.value) -- 输出:10(因为通过元表继承了 base 表中的值)
当 derived
表中没有找到 value
这个键时,Lua会去它的元表中查找 __index
,如果 __index
指向了另一个表 base
,它就会从 base
中获取 value
的值。这种机制实现了类似继承的效果。
2.2.6 总结
- 元表(Metatable)是Lua的一种机制,允许为表添加新的行为。
- 元方法(Metamethods)是定义在元表中的特殊函数,用来处理如运算符重载、索引访问等操作。
- 通过元表和元方法,你可以为表自定义行为,比如定义如何进行加法、索引不存在时的默认行为,甚至模拟面向对象编程中的继承。
3 Lua 面向对象范式
3.1 使用 . 定义函数和使用 : 定义函数的区别
在 Lua 中,使用 .
和 :
定义和调用函数有着重要的区别,主要体现在函数的调用方式和隐式传递的参数上。具体来说,:
是用于定义和调用方法(method),而 .
是用于定义和调用普通函数(function)。理解这一点,才能更好的理解 Lua 是怎么通过表来实现面向对象编程的。
1. 使用 .
定义和调用函数
使用 .
定义的函数是普通函数,调用时需要显式传递所有参数。
-- 定义一个表
local myTable = {}
-- 使用 . 定义一个普通函数
function myTable.sayHello(name)
print("Hello, " .. name)
end
-- 调用普通函数
myTable.sayHello("Alice") -- 输出:Hello, Alice
2. 使用 :
定义和调用方法
使用 :
定义的函数是方法,调用时会隐式传递调用者(即表本身)作为第一个参数 self
。
-- 定义一个表
local myTable = {}
-- 使用 : 定义一个方法
function myTable:sayHello(name)
print("Hello, " .. name)
print("Called by", self)
end
-- 调用方法
myTable:sayHello("Alice") -- 输出:Hello, Alice
-- Called by table: 0x...
在上面的例子中,myTable:sayHello("Alice")
实际上等价于 myTable.sayHello(myTable, "Alice")
。也就是说,调用者 myTable
被隐式地作为第一个参数传递给方法 sayHello
,并在方法内部作为 self
使用。
3. 具体区别总结
-
定义方式:
function tableName.functionName(args)
:定义普通函数。function tableName:functionName(args)
:定义方法,隐式传递self
。
-
调用方式:
tableName.functionName(args)
:调用普通函数,显式传递所有参数。tableName:functionName(args)
:调用方法,隐式传递self
作为第一个参数。
3.1.4 示例对比
- 使用
.
定义和调用普通函数
local myTable = {}
function myTable.sayHello(name)
print("Hello, " .. name)
end
myTable.sayHello("Alice") -- 输出:Hello, Alice
- 使用
:
定义和调用方法
local myTable = {}
function myTable:sayHello(name)
print("Hello, " .. name)
print("Called by", self)
end
myTable:sayHello("Alice") -- 输出:Hello, Alice
-- Called by table: 0x...
- 适用场景
- 使用
.
定义和调用普通函数时,适用于不需要引用调用者的场景。 - 使用
:
定义和调用方法时,适用于需要引用调用者(即表本身)的场景,例如在面向对象编程中定义类的方法。 - 通过理解这两种方式的区别,可以更灵活地在 Lua 中定义和调用函数,编写出更清晰和结构化的代码。
3.2 实现面向对象编程
虽然 Lua 本身没有内置的面向对象编程支持,但可以通过元表(metatables)和表(tables)来实现面向对象编程。
-- 定义一个类
Person = {}
Person.__index = Person
-- 构造函数
function Person:new(name, age)
local self = setmetatable({}, Person)
self.name = name
self.age = age
return self
end
-- 方法
function Person:greet()
print("Hello, my name is " .. self.name .. " and I am " .. self.age .. " years old.")
end
-- 创建对象
local person = Person:new("Alice", 30)
person:greet()
4 Lua与UE引擎的交互
Lua 与 Unreal Engine(UE)交互通常通过第三方插件或绑定库来实现。这些插件和库提供了在 UE 中嵌入 Lua 脚本的能力,使得开发者可以使用 Lua 编写游戏逻辑、控制游戏对象等。以下是一些常见的方法和工具:
4.1 使用第三方插件 UnLua
UnLua 是一个专门为 Unreal Engine 设计的 Lua 插件,提供了深度集成和高性能。以下是使用 UnLua 的基本步骤:
-
安装 UnLua:
- 下载并安装 UnLua 插件。
- 将插件添加到你的 UE 项目中。
-
配置 UnLua:
- 在项目设置中启用 UnLua 插件。
- 配置 Lua 脚本路径等参数。
-
编写 Lua 脚本:
-
创建 Lua 脚本文件,例如
MyScript.lua
。 -
编写游戏逻辑,例如:
print("Hello from UnLua!") function OnBeginPlay() print("Game started") end
-
-
在 UE 中调用 Lua 脚本:
-
在 UE 蓝图或 C++ 代码中加载并执行 Lua 脚本。
// 在 C++ 代码中加载 Lua 脚本 UUnLuaManager* UnLuaManager = UUnLuaManager::Get(); UnLuaManager->RunFile("MyScript.lua"); // 调用 Lua 函数 UnLuaManager->CallFunction("OnBeginPlay");
-
4.2 使用 Unreal Engine Lua Plugin
Unreal Engine Lua Plugin 是一个流行的插件,允许在 UE 中嵌入 Lua 脚本。以下是使用该插件的一些基本步骤:
-
安装插件:
- 下载并安装 Unreal Engine Lua Plugin。
- 将插件添加到你的 UE 项目中。
-
配置插件:
- 在项目设置中启用 Lua 插件。
- 配置 Lua 脚本路径等参数。
-
编写 Lua 脚本:
-
创建 Lua 脚本文件,例如
MyScript.lua
。 -
编写游戏逻辑,例如:
print("Hello from Lua!") function OnBeginPlay() print("Game started") end
-
-
在 UE 中调用 Lua 脚本:
-
在 UE 蓝图或 C++ 代码中加载并执行 Lua 脚本。
// 在 C++ 代码中加载 Lua 脚本 ULuaState* LuaState = NewObject<ULuaState>(); LuaState->DoFile("MyScript.lua"); // 调用 Lua 函数 LuaState->GetFunction("OnBeginPlay"); LuaState->Call(0, 0);
-
4.3 Lua和UE交互的实现原理
Lua 与 Unreal Engine(UE)交互的底层实现原理主要涉及以下几个方面:
-
嵌入 Lua 解释器:
- 在 UE 中嵌入 Lua 解释器,使得 Lua 脚本可以在 UE 的运行时环境中执行。
- 这通常通过在 C++ 代码中包含 Lua 解释器库(如
lua.hpp
)并初始化 Lua 解释器来实现。
-
绑定 C++ 和 Lua:
- 通过绑定机制,将 UE 的 C++ 类和函数通过反射机制暴露给 Lua,使得 Lua 脚本可以调用这些 C++ 函数。
- 绑定机制可以手动实现,也可以使用自动化工具或库(如 LuaBridge、Sol2、UnLua 等)来简化绑定过程。
-
脚本加载和执行:
- 提供加载和执行 Lua 脚本的功能,使得 Lua 脚本可以在特定的事件或条件下执行。
- 这通常通过在 C++ 代码中调用 Lua 解释器的 API 来实现,例如
luaL_dofile
用于加载和执行 Lua 脚本。
-
事件和回调机制:
- 实现事件和回调机制,使得 Lua 脚本可以响应 UE 中的事件(如游戏开始、对象碰撞等)。
- 这通常通过在 C++ 代码中注册 Lua 函数作为回调函数,并在特定事件发生时调用这些回调函数来实现。
以下是一些具体的实现细节,展示了如何在 C++ 代码中嵌入 Lua 解释器并实现与 Lua 的交互。
- 嵌入 Lua 解释器
首先,需要在 C++ 代码中包含 Lua 解释器库并初始化 Lua 解释器:
#include "lua.hpp"
lua_State* L = luaL_newstate(); // 创建一个新的 Lua 状态
luaL_openlibs(L); // 打开 Lua 标准库
- 绑定 C++ 和 Lua
可以使用 LuaBridge 或其他绑定库来简化绑定过程。以下是使用 LuaBridge 的示例:
#include "LuaBridge/LuaBridge.h"
void HelloWorld()
{
UE_LOG(LogTemp, Log, TEXT("Hello from C++"));
}
void BindFunctions(lua_State* L)
{
luabridge::getGlobalNamespace(L)
.addFunction("HelloWorld", HelloWorld);
}
在 Lua 脚本中,可以调用绑定的 C++ 函数:
HelloWorld() -- 调用 C++ 函数
- 脚本加载和执行
可以在 C++ 代码中加载和执行 Lua 脚本:
if (luaL_dofile(L, "MyScript.lua") != LUA_OK)
{
const char* error = lua_tostring(L, -1);
UE_LOG(LogTemp, Error, TEXT("Error: %s"), UTF8_TO_TCHAR(error));
}
- 事件和回调机制
可以在 C++ 代码中注册 Lua 函数作为回调函数,并在特定事件发生时调用这些回调函数:
// 注册 Lua 回调函数
lua_getglobal(L, "OnBeginPlay");
if (lua_isfunction(L, -1))
{
lua_pcall(L, 0, 0, 0);
}
在 Lua 脚本中定义回调函数:
function OnBeginPlay()
print("Game started")
end
5. 总结
Lua 与 Unreal Engine 交互的底层实现原理主要涉及嵌入 Lua 解释器、绑定 C++ 和 Lua、加载和执行 Lua 脚本以及实现事件和回调机制。通过这些机制,可以在 UE 中嵌入 Lua 脚本,实现灵活的游戏逻辑编写和控制。使用第三方插件和库(如 UnLua、LuaBridge 等)可以简化这些过程,使得开发者更容易实现 Lua 与 UE 的交互。
5 Lua和C++的不同之处
5.1 变量的赋值规则
5.1.1 C++的基本变量赋值和对象赋值
基本变量赋值就是值拷贝的过程,对于对象赋值才有浅拷贝和深拷贝的区分。
浅拷贝
是指复制对象时,只复制对象的基本数据成员,而不复制指向的资源(如动态分配的内存)。这意味着两个对象将共享同一块内存资源。深拷贝
是指复制对象时,不仅复制对象的基本数据成员,还复制指向的资源。这意味着每个对象都有自己独立的内存资源。
浅拷贝发生的场景:
- 对于内置类型(如int、double、char等)和简单的结构体,赋值操作是值拷贝,因为这些类型的赋值操作只是复制其值。(,“浅拷贝” 和 “深拷贝” 通常用于描述对象(如数组、结构体、类实例等)的复制行为,而不是基本数据类型的复制行为。)
- 对于类对象,默认的赋值操作符(operator=)是
浅拷贝
。默认的赋值操作符会逐个成员地进行赋值,这对于简单类型的成员是浅拷贝,但对于指针成员则只是复制指针地址。
如果需要深拷贝,可以自定义赋值操作符和拷贝构造函数。
5.1.2 Lua的不同赋值操作
- 基本类型赋值
Lua对于基本类型(如数字、字符串、布尔值、nil),赋值操作是值拷贝。这意味着赋值操作会创建一个新副本,两个变量之间没有任何关联。这里的值拷贝并不涉及对象的引用,因此不适用浅拷贝和深拷贝的概念。
local a = 10
local b = a -- 值拷贝
print(a) -- 输出: 10
print(b) -- 输出: 10
b = 20
print(a) -- 输出: 10
print(b) -- 输出: 20
在这个例子中,a 和 b 是独立的变量,修改 b 不会影响 a。
- 引用类型赋值
对于引用类型(如表、函数、用户数据
),赋值操作是引用拷贝(即浅拷贝
)。这意味着赋值操作不会创建新副本,而是让两个变量引用同一个对象。
local t1 = {1, 2, 3}
local t2 = t1 -- 引用拷贝
print(t1[1]) -- 输出: 1
print(t2[1]) -- 输出: 1
t2[1] = 10
print(t1[1]) -- 输出: 10
print(t2[1]) -- 输出: 10
在这个例子中,t1 和 t2 引用同一个表,修改 t2 会影响 t1。如果需要进行深拷贝,需要自己实现一个深拷贝函数。
3. Lua引用和C++指针的区别
Lua 的引用类型和 C++ 的指针有一些相似之处,但也有显著的不同:
- 相似之处:
- 都可以让多个变量引用同一个对象。
- 修改一个变量的内容会影响所有引用该对象的变量。
- 不同之处:
- 语法:Lua 没有显式的指针语法,引用类型的赋值和使用与基本类型没有区别。而在 C++ 中,指针有特定的语法(如 * 和 & 操作符)。
- 内存管理:Lua 使用垃圾回收机制自动管理内存,而 C++ 通常需要手动管理内存(除非使用智能指针)。
- 类型安全:Lua 是动态类型语言,引用类型的变量可以在运行时改变其引用的对象类型。而 C++ 是静态类型语言,指针类型在编译时确定。
5.2 动态类型和静态类型
Lua是动态类型语言,C++是静态类型语言,但是提供一些动态类型检查的方法,例如 DynamicCast 。这两种类型系统有着显著的区别,影响了它们的编程风格和使用场景。
5.2.1 动态类型语言(Lua)
在动态类型语言中,变量的类型是在运行时确定的,而不是在编译时确定的。Lua 就是这样一种语言。以下是动态类型语言的一些特点:
- 类型检查在运行时进行:变量的类型是在程序运行时确定的,而不是在编译时确定的。
- 灵活性高:由于类型是在运行时确定的,变量可以在不同的时间点持有不同类型的值。
- 代码简洁:不需要显式声明变量的类型,代码通常更简洁。
- 易于使用:对于快速原型开发和脚本编写非常方便。
代码示例
local x = 10 -- x 是一个数字
print(x) -- 输出:10
x = "Hello" -- x 现在是一个字符串
print(x) -- 输出:Hello
x = {1, 2, 3} -- x 现在是一个表
print(x[1]) -- 输出:1
在这个示例中,变量 x
可以在不同的时间点持有不同类型的值。
同时Lua 提供了一个内置的 type
函数,可以用来检查变量的类型。type
函数返回一个字符串,表示变量的类型。或者可以使用 assert
函数来进行类型检查,并在类型不匹配时抛出错误。assert
函数接受一个条件和一个可选的错误消息,如果条件为假,则抛出错误。
5.2.2 静态类型语言(C++)
在静态类型语言中,变量的类型是在编译时确定的。C++ 就是这样一种语言。以下是静态类型语言的一些特点:
- 类型检查在编译时进行:变量的类型是在编译时确定的,编译器会在编译时进行类型检查。
- 类型安全:由于类型在编译时确定,许多类型错误可以在编译时被发现,从而提高了程序的 安全性 。
- 性能高:由于类型在编译时确定,编译器可以进行更多的优化,从而提高程序的运行效率。
- 代码冗长:需要显式声明变量的类型,代码通常更冗长。
代码示例
#include <iostream>
#include <string>
int main() {
int x = 10; // x 是一个整数
std::cout << x << std::endl; // 输出:10
std::string y = "Hello"; // y 是一个字符串
std::cout << y << std::endl; // 输出:Hello
// x = "Hello"; // 错误:不能将字符串赋值给整数
return 0;
}
在这个示例中,变量 x
和 y
的类型在编译时就已经确定,并且不能改变。
虽然C++是静态类型语言,但是还是有很多手段进行动态类型检查的。包括 dynamic_cast
、typeid
和 std::type_info
、自定义类型检查
以及结合智能指针和类型擦除技术。(挖个坑,以后填)
5.2.3 总结
-
Lua(动态类型语言):
- 类型在运行时确定。
- 变量可以在不同时间点持有不同类型的值。
- 代码更简洁,适合快速原型开发和脚本编写。
-
C++(静态类型语言):
- 类型在编译时确定。
- 变量的类型一旦确定就不能改变。
- 代码更冗长,但类型安全性更高,性能更好。
这两种类型系统各有优缺点,适用于不同的编程场景。动态类型语言提供了更高的灵活性和更快的开发速度,而静态类型语言提供了更高的类型安全性和性能。
5.3 垃圾回收
Lua具有垃圾回收功能,C++没有。Lua的垃圾回收主要作用于动态分配的内存对象,例如,表、函数、用户数据、线程、字符串。
为了更好的理解Lua垃圾回收的过程。引入一个场景,在 Lua 中,当你将 t1
置为 nil
时,再去访问 t2
,t2
仍然会保持对原始表的引用。
local t1 = { 1 }
local t2 = t1
t1 = nil
if t2 == nil then
-- 这个块不会被执行
end
-
创建表并赋值给
t1
local t1 = { 1 }
这行代码创建了一个包含一个元素
1
的表,并将其引用赋值给变量t1
。 -
将
t1
的引用赋值给t2
:local t2 = t1
这行代码将
t1
的引用赋值给t2
,此时t1
和t2
都指向同一个表。 -
将
t1
置为nil
:t1 = nil
这行代码将
t1
置为nil
,此时t1
不再引用那个表,但t2
仍然引用着那个表。 -
检查
t2
是否为nil
:if t2 == nil then -- 这个块不会被执行 end
由于
t2
仍然引用着那个表,所以这个表不会被垃圾回收,所以t2
不为nil
,因此这个条件判断为false
,代码块不会被执行。
需要注意的是,Lua 的垃圾回收机制会在没有任何变量引用某个对象时回收其内存。在这个例子中,虽然 t1
被置为 nil
,但 t2
仍然引用着那个表,所以垃圾回收器不会回收这个表的内存。只有当 t2
也被置为 nil
或引用其他对象时,垃圾回收器才会回收这个表的内存。
t2 = nil
collectgarbage() -- 手动触发垃圾回收
在这之后,原始的表将不再被引用,垃圾回收器会在适当的时候回收其内存。
5.4 弱引用
就刚才的代码示例而言,有些时候希望t2只是弱引用t1的表。在t1将表取消引用的时候,就将这块内存区域给回收。
在 Lua 中,可以使用弱引用来实现某些高级内存管理策略。弱引用允许你创建一种特殊的表,这种表中的键或值不会阻止垃圾回收器回收它们所引用的对象。Lua 提供了 setmetatable
和 __mode
元方法来实现弱引用。
Lua 支持两种类型的弱引用表:
- 弱键表:表的键是弱引用。
- 弱值表:表的值是弱引用。
你可以通过设置元表的 __mode
字段来指定弱引用的类型:
"k"
表示弱键。"v"
表示弱值。"kv"
表示弱键和弱值。
以下是一个示例,展示如何创建一个弱值表,使得 t2
弱引用 t1
存储的表:
-- 创建一个弱值表
local weakTable = setmetatable({}, { __mode = "v" })
-- 创建一个表并赋值给 t1
local t1 = { 1 }
-- 将 t1 存储的表弱引用赋值给 weakTable
weakTable["key"] = t1
-- 将 t1 置为 nil
t1 = nil
-- 手动触发垃圾回收
collectgarbage()
-- 检查 weakTable 中的值是否被回收
if weakTable["key"] == nil then
print("The table has been garbage collected.")
else
print("The table is still accessible.")
print(weakTable["key"][1]) -- 输出: 1
end
详细解释
-
创建弱值表:
local weakTable = setmetatable({}, { __mode = "v" })
这行代码创建了一个弱值表,表中的值是弱引用。
-
创建表并赋值给
t1
:local t1 = { 1 }
这行代码创建了一个包含一个元素
1
的表,并将其引用赋值给变量t1
。 -
将
t1
存储的表弱引用赋值给weakTable
:weakTable["key"] = t1
这行代码将
t1
存储的表的引用赋值给weakTable
,但由于weakTable
是弱值表,所以这个引用是弱引用。 -
将
t1
置为nil
:t1 = nil
这行代码将
t1
置为nil
,此时没有强引用指向那个表。 -
手动触发垃圾回收:
collectgarbage()
这行代码手动触发垃圾回收,垃圾回收器会回收所有不再被强引用的对象。
-
检查
weakTable
中的值是否被回收:if weakTable["key"] == nil then print("The table has been garbage collected.") else print("The table is still accessible.") print(weakTable["key"][1]) -- 输出: 1 end
由于
weakTable
中的值是弱引用,当t1
被置为nil
后,那个表不再有强引用,因此会被垃圾回收器回收。
运行这段代码会输出:
The table has been garbage collected.
这表明当 t1
被置为 nil
后,weakTable
中的值也被垃圾回收器回收了,因为它是一个弱引用。