【UE Lua】 快速入门(基础语法、与UE引擎的交互原理、与C++的不同)

请添加图片描述

  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:UE虚幻引擎专栏
  • 💥 标题:【UE Lua】 快速入门(基础语法、与UE引擎的交互原理、与C++的不同)
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!
  • 🎈 最后:文章作者技术和水平有限,如果文中出现错误,希望大家能指正,同时有问题的话,欢迎大家留言讨论。

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} 这种表,它既包含了单个值(13,也就是整数索引的值),又包含了键值对(name = "nihao",这是非整数键的键值对)。在Lua内部的存储中,会将这种混合形式分别存储在数组部分哈希部分

存储方式:

  1. 数组部分:存储具有连续整数索引的值。

    • 在这个例子中,13 分别是整数索引 1 和 2 的值,符合数组部分的存储规则,所以它们会被放在数组部分。
  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. 具体区别总结

  1. 定义方式

    • function tableName.functionName(args):定义普通函数。
    • function tableName:functionName(args):定义方法,隐式传递 self
  2. 调用方式

    • tableName.functionName(args):调用普通函数,显式传递所有参数。
    • tableName:functionName(args):调用方法,隐式传递 self 作为第一个参数。

3.1.4 示例对比

  1. 使用 . 定义和调用普通函数
local myTable = {}

function myTable.sayHello(name)
    print("Hello, " .. name)
end

myTable.sayHello("Alice")  -- 输出:Hello, Alice
  1. 使用 : 定义和调用方法
local myTable = {}

function myTable:sayHello(name)
    print("Hello, " .. name)
    print("Called by", self)
end

myTable:sayHello("Alice")  -- 输出:Hello, Alice
                           --       Called by table: 0x...
  1. 适用场景
  • 使用 . 定义和调用普通函数时,适用于不需要引用调用者的场景。
  • 使用 : 定义和调用方法时,适用于需要引用调用者(即表本身)的场景,例如在面向对象编程中定义类的方法。
  • 通过理解这两种方式的区别,可以更灵活地在 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 的基本步骤:

  1. 安装 UnLua

    • 下载并安装 UnLua 插件。
    • 将插件添加到你的 UE 项目中。
  2. 配置 UnLua

    • 在项目设置中启用 UnLua 插件。
    • 配置 Lua 脚本路径等参数。
  3. 编写 Lua 脚本

    • 创建 Lua 脚本文件,例如 MyScript.lua

    • 编写游戏逻辑,例如:

      print("Hello from UnLua!")
      
      function OnBeginPlay()
          print("Game started")
      end
      
  4. 在 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 脚本。以下是使用该插件的一些基本步骤:

  1. 安装插件

    • 下载并安装 Unreal Engine Lua Plugin。
    • 将插件添加到你的 UE 项目中。
  2. 配置插件

    • 在项目设置中启用 Lua 插件。
    • 配置 Lua 脚本路径等参数。
  3. 编写 Lua 脚本

    • 创建 Lua 脚本文件,例如 MyScript.lua

    • 编写游戏逻辑,例如:

      print("Hello from Lua!")
      
      function OnBeginPlay()
          print("Game started")
      end
      
  4. 在 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)交互的底层实现原理主要涉及以下几个方面:

  1. 嵌入 Lua 解释器

    • 在 UE 中嵌入 Lua 解释器,使得 Lua 脚本可以在 UE 的运行时环境中执行。
    • 这通常通过在 C++ 代码中包含 Lua 解释器库(如 lua.hpp)并初始化 Lua 解释器来实现。
  2. 绑定 C++ 和 Lua

    • 通过绑定机制,将 UE 的 C++ 类和函数通过反射机制暴露给 Lua,使得 Lua 脚本可以调用这些 C++ 函数。
    • 绑定机制可以手动实现,也可以使用自动化工具或库(如 LuaBridge、Sol2、UnLua 等)来简化绑定过程。
  3. 脚本加载和执行

    • 提供加载和执行 Lua 脚本的功能,使得 Lua 脚本可以在特定的事件或条件下执行。
    • 这通常通过在 C++ 代码中调用 Lua 解释器的 API 来实现,例如 luaL_dofile 用于加载和执行 Lua 脚本。
  4. 事件和回调机制

    • 实现事件和回调机制,使得 Lua 脚本可以响应 UE 中的事件(如游戏开始、对象碰撞等)。
    • 这通常通过在 C++ 代码中注册 Lua 函数作为回调函数,并在特定事件发生时调用这些回调函数来实现。

以下是一些具体的实现细节,展示了如何在 C++ 代码中嵌入 Lua 解释器并实现与 Lua 的交互。


  1. 嵌入 Lua 解释器

首先,需要在 C++ 代码中包含 Lua 解释器库并初始化 Lua 解释器:

#include "lua.hpp"

lua_State* L = luaL_newstate();  // 创建一个新的 Lua 状态
luaL_openlibs(L);  // 打开 Lua 标准库
  1. 绑定 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++ 函数
  1. 脚本加载和执行

可以在 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));
}
  1. 事件和回调机制

可以在 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的不同赋值操作

  1. 基本类型赋值

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。

  1. 引用类型赋值

对于引用类型(如表、函数、用户数据),赋值操作是引用拷贝(即浅拷贝)。这意味着赋值操作不会创建新副本,而是让两个变量引用同一个对象。

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 就是这样一种语言。以下是动态类型语言的一些特点:

  1. 类型检查在运行时进行:变量的类型是在程序运行时确定的,而不是在编译时确定的。
  2. 灵活性高:由于类型是在运行时确定的,变量可以在不同的时间点持有不同类型的值
  3. 代码简洁:不需要显式声明变量的类型,代码通常更简洁。
  4. 易于使用:对于快速原型开发和脚本编写非常方便。

代码示例

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++ 就是这样一种语言。以下是静态类型语言的一些特点:

  1. 类型检查在编译时进行:变量的类型是在编译时确定的,编译器会在编译时进行类型检查。
  2. 类型安全:由于类型在编译时确定,许多类型错误可以在编译时被发现,从而提高了程序的 安全性
  3. 性能高:由于类型在编译时确定,编译器可以进行更多的优化,从而提高程序的运行效率。
  4. 代码冗长:需要显式声明变量的类型,代码通常更冗长。

代码示例

#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;
}

在这个示例中,变量 xy 的类型在编译时就已经确定,并且不能改变。

虽然C++是静态类型语言,但是还是有很多手段进行动态类型检查的。包括 dynamic_casttypeidstd::type_info自定义类型检查以及结合智能指针和类型擦除技术。(挖个坑,以后填)

5.2.3 总结

  • Lua(动态类型语言)

    • 类型在运行时确定。
    • 变量可以在不同时间点持有不同类型的值。
    • 代码更简洁,适合快速原型开发和脚本编写。
  • C++(静态类型语言)

    • 类型在编译时确定。
    • 变量的类型一旦确定就不能改变。
    • 代码更冗长,但类型安全性更高,性能更好。

这两种类型系统各有优缺点,适用于不同的编程场景。动态类型语言提供了更高的灵活性和更快的开发速度,而静态类型语言提供了更高的类型安全性和性能。

5.3 垃圾回收

Lua具有垃圾回收功能,C++没有。Lua的垃圾回收主要作用于动态分配的内存对象,例如,表、函数、用户数据、线程、字符串。

为了更好的理解Lua垃圾回收的过程。引入一个场景,在 Lua 中,当你将 t1 置为 nil 时,再去访问 t2t2 仍然会保持对原始表的引用。

local t1 = { 1 }
local t2 = t1
t1 = nil
if t2 == nil then
    -- 这个块不会被执行
end
  1. 创建表并赋值给 t1

    local t1 = { 1 }
    

    这行代码创建了一个包含一个元素 1 的表,并将其引用赋值给变量 t1

  2. t1 的引用赋值给 t2

    local t2 = t1
    

    这行代码将 t1 的引用赋值给 t2,此时 t1t2 都指向同一个表。

  3. t1 置为 nil

    t1 = nil
    

    这行代码将 t1 置为 nil,此时 t1 不再引用那个表,但 t2 仍然引用着那个表。

  4. 检查 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 支持两种类型的弱引用表:

  1. 弱键表:表的键是弱引用。
  2. 弱值表:表的值是弱引用。

你可以通过设置元表的 __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

详细解释

  1. 创建弱值表

    local weakTable = setmetatable({}, { __mode = "v" })
    

    这行代码创建了一个弱值表,表中的值是弱引用。

  2. 创建表并赋值给 t1

    local t1 = { 1 }
    

    这行代码创建了一个包含一个元素 1 的表,并将其引用赋值给变量 t1

  3. t1 存储的表弱引用赋值给 weakTable

    weakTable["key"] = t1
    

    这行代码将 t1 存储的表的引用赋值给 weakTable,但由于 weakTable 是弱值表,所以这个引用是弱引用。

  4. t1 置为 nil

    t1 = nil
    

    这行代码将 t1 置为 nil,此时没有强引用指向那个表。

  5. 手动触发垃圾回收

    collectgarbage()
    

    这行代码手动触发垃圾回收,垃圾回收器会回收所有不再被强引用的对象。

  6. 检查 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 中的值也被垃圾回收器回收了,因为它是一个弱引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值