BRabbit's Blog

This blog has super rabbit power!

table 进阶

数据类型-table中提到 : “粗浅的理解可将table视作 array 与 map 的合体” ;
更准确的来说应该是 : “table是Lua中用于构建不同数据类型的类型, 如 array 与 map 等” .
甚至Lua中的module, package, Object这些概念都是基于table所构建的 , table在Lua中的重要性可见一斑 .

成员方法 : 与string一样 , Lua也提供了table类 , 其包含了许多成员方法用于方便的操作table .

对table的使用方式

  • 作为Array使用
  • 作为Map使用
  • 作为Module与Package使用
  • 作为Object使用

table的引用赋值

Lua中table的赋值实质的引用 , 若两个table变量同时引用了同一块资源 , 当删除一个变量时资源不会被删除 .
只有最后一个引用该资源的变量被删除了 , 资源才会被释放 .


将 table 用作 array

使用table时不指定元素的key , 并且使用[]来选定元素 , 即可实现 array 的功能 .

Lua table 使用关联型数组 , 可在一个数组中插入不同类型的值(nil除外) . 同时其也是不定长的 , 可任意扩充内容 .
(将table用作array在使用上更像使用C++STL中的vector)

示例 (Code/tableLua/arrayTable.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 数组测试
local array = {"Lua", "array", "test"} -- 创建数组
local lineStr = ""
for i = 1, 3 do -- 遍历数组
lineStr = lineStr .. array[i] .. " " -- 通过下表取得元素
end
print(lineStr)
-- 多维数组测试
local multiArray = {{1, 2, 3}, -- 创建三维数组
{4, 5, 6},
{7, 8, 9}}
for i = 1, 3 do -- 遍历并输出三维数组
local lineStr = ""
for j = 1, 3 do
lineStr = lineStr .. multiArray[i][j] .. " "
end
print(lineStr)
end
print(#multiArray) -- 输出多维数组的行数

输出

4.arrayTable.lua输出.png


将 table 作为 map

使用table时以键值对的形式存入元素 , 并用.来指定特定key的value , 即可实现 map 的功能 .

Lua中table底层设计参考了散列表 , 因此其键值对在容器中是无序的 . 使用上更接近C++中的unordered_map .

示例 (Code/tableLua/mapTable.lua)

1
2
3
4
5
6
7
local map = {}       -- 创建字典
map.key1 = "value1" -- 向字典中插入键值对
map.key2 = "value2"
map.key3 = "value3"
for key, value in pairs(map) do -- 遍历输出键值对
print(key .. " : " .. value)
end

输出

4.mapTable.lua输出.png


将 table 作为 Module 与 Package

将模块/包的相关属性与方法放入同一个table中 , 使用.调用 , 即可实现模块与包的功能 .

在Lua中 , function 是数据类型的一种 , 同时 table 允许存入不同类型的变量 .
将以上两种特性结合一下 , 即可使用 table 实现模块与包的功能 .

创建模块

  1. 在单独的lua文件中创建一个table
    module = {}
  2. 写入该模块的常量与函数
    module.constant = xxx / function module.fun() xxx end
  3. 返回该table
    return module

加载模块

  • Lua模块 : 使用 require('模块名')require '模块名' 加载Lua模块
  • C包 : 使用 package.loadlib("路径") 加载模块 , 使用 assert(模块) 打开模块

示例 (Code/tableLua/moduleTable.lua 与 testModule.lua)

moduleTable.lua

1
2
3
4
5
6
7
8
9
10
-- 自定义模块 module
module = {} -- 创建表
module.constant = "MODULE" -- 定义常量
local function privateFunc() -- 定义私有函数
print("I AM " .. module.constant)
end
function module.testFunc() -- 定义公有函数
privateFunc()
end
return module -- 返回表

testModule.lua

1
2
require "moduleTable"  -- 加载模块
module.publicFunc() -- 调用模块

testModule.lua输出

4.testModule.lua输出.png


将 table 作为 Object

Lua中 , 模块与包的功能也可基于table实现 , 同样的也可以利用table实现对象的功能 .

但基于现有的知识还不能完全实现对象的功能 :

  • 虽然可以通过table封装变量与方法 , 但无法实现类的实例化 (或者说只能对类操作 , 无法对对象操作)
  • 当学习过 Metatable 元表 后 , 即可配合元表实现 类与对象的功能 了, 进一步还可以实现OOP .

table成员方法

Lua提供了table类 , 其有很多实用的成员方法提供对表的操作 . (这里的table不是代指某一字符串 , 而是一个类叫做table)

table.concat(list, [sep], [i], [j])

元素拼接 : 列表list中所有元素都是字符串或数字 , 返回字符串 list[i]list[j] , 中间间隔sep .
(sep缺省nil, i缺省1, j缺省#list)

1
2
local tab = {"123", 456, "789"}
table.concat(tab, "a", 1, #tab) --> 返回 123a456a789

table.insert(list, pos, value)

插入元素 : 往表listpos位置插入一个值为value的元素 , 无返回值 .

1
2
local tab = {123, 789}
table.insert(tab, 2, 456) --> 插入后的tab {1:123 , 2:456 , 3:789}

table.remove(list, [pos])

删除元素 : 删除表listpos位置的元素 , 返回被删除的值 . (pos缺省#list , 即最后一个 , 只能是数字)

1
2
local tab = {k1="v1", 123, k2="v2", 456}
table.remove(tab, 2) --> 返回456 , tab = {k1:"v1", 1:123, k2:"v2"}

table.sort(list, [comp])

表排序 : 将表list根据函数comp的规则排序 , 无返回值 .
(comp为两个参数返回值为boolean的函数 , 类似C++STL中的仿函数 , 缺省值是一个顺序排序的函数)

1
2
3
4
5
local tab = {3, 1, 4, 2}
local function forwardSort(a, b) -- 与[comp]的缺省值功能相同
if a < b then return true else return false end
end
table.sort(tab, forwardSort) --> tab = {1:1, 2:2, 3:3, 4:4}

table.pack(…) (Lua 5.2新增)

包装表 : 将所有参数作为值依次填入新表并返回 , 并在最后增加一个键值对 n : 参数个数

1
2
local newTab = table.pack("v1", 123, "v2", 456)
-- newTab = {1:"v1" , 2:132 , 3:"v2" , 4:456 , n:4}

table.unpack(list, [i], [j]) (Lua 5.2新增)

拆分表 : 返回表list中位置从ij的元素的值 . (list内元素不变)
(i缺省1, j缺省#list)

1
2
3
local tab = {3, 1, 4, 2}
table.unpack(tab, 2, 3) --> 返回 1 4
local newTab = table.pack(table.unpack(tab, 2, 3)) -->newTab={1:1 , 2:4 , n:2}

table.move(a1, f, e, t, [a2]) (Lua 5.3新增)

移动元素 : 将表a1中索引从fe的元素 , 移动到表a2索引为t的位置之后 , 无返回值 .
(a2缺省a1)

1
2
local tab = {123, 456, 789}
tab = table.move(tab, 1, 2, 3) --> 返回 {1:123 , 2:456 , 3:123 , 4:456}

table.maxn(table) (Lua 5.2弃用)

获取最大值 : 返回表table的最大正数索引,如果表没有正数索引,则返回零。

string 进阶

Lua中有三种方式表达一个字符串 :

  • 用单引号包围 : ' string ' ;
  • 用双引号包围 : " string " ;
  • 用双中括号包围 : [[ string ]] ;

为了方便地使用字符串 , Lua提供了如下”功能” :

  • 转义字符 : 转义字符用于表示字符串中不能直接显示的字符 ;
  • 成员方法 : string类有很多实用的成员方法 ;
  • 格式化 : 类似C中printf()的字符串格式化 , 常用于string.format()方法;
  • 匹配模式 : 类似正则表达式一样的匹配 , 常用于string.find(), string.gmatch(), string.gsub(), string.match()方法 ;

转义字符

Lua中字符串的转义字符与C/C++一样 , 都是一个\加上特殊的字符 . 个别语法上有些许差别 .

转义字符 意义 ASCII码 (十进制)
\a 响铃 (BEL) . 让系统发出一个提示音 007
\b 退格 (BS) . 将当前位置移动到前一列 008
\f 换页 (FF) . 将当前位置移动到下一页开头 012
\n 换行 (CR) . 将当前位置移动到下一行开头 010
\r 回车 (CR) . 将当前位置移动到本行开头 013
\t 水平制表 (HT) . 跳到下一个TAB位置 009
\v 垂直制表 (VT) 011
\\ \ 一个放斜杠字符 092
\‘ 一个单引号字符 039
\“ 一个双引号字符 034
\0 空字符 (NULL) 000
\ddd 1-3位八进制数 (d代表数字 , 数量可调整) 3位八进制
\xhh 1-2位十六进制数 (h代表数字 , 数量可调整) 2位十六进制

string成员方法

Lua提供了string类 , 其有很多实用的成员方法提供对字符串的操作 . (这里的string不是代指某一字符串 , 而是一个类叫做string)

string.upper(s) / string.lower(s)

转换大小写 . 前者将参数字符串s转换为大写字母并返回 , 后者将参数字符串s转换为小写字母并返回 .

1
2
string.upper("qwerty")  --> 返回 QWERTY
string.lower("QWERTY") --> 返回 qwerty

string.gsub(s, pattern, repl, [n])

替换字符串 . s字符串中npattern字符替换为repl字符 , n缺省为#str(替换全部) .

1
string.gsub("aaaa", "a", "z", 3)  --> 返回 zzza

string.find(s, pattern, [init], [plain])

搜索字符串 . 在s中从init处开始搜索第1个出现的pattern, 返回其起始位置和结束位置 . init缺省1, plain不知道啥用 .

1
string.find("Hello Lua user", "Lua ", 1)  --> 返回 7 10 (匹配的是"Lua "有一个空格)

string.reverse(s)

反转字符串 . 将参数字符串s反转后返回 .

1
string.reverse("Lua") --> 返回 auL

string.format(s, …)

字符串格式化 . 类似C中的printf() , 但结果作为返回值返回而不是输出 .

1
string.format("the value is : %d", 4) --> 返回 the value is : 4 (是返回不是输出)

string.char(byte, …) / string.byte(s, [i], [j])

字符串转换 . 前者将整型按照ASCII码转换为字符并拼接 , 后者将字符串s的第ij个字符转换为整型 .
(string.byte中 : i缺省1, j缺省i, 负数表示从末尾开始的某个字符)

1
2
3
4
string.char(97, 98, 99, 100)  --> 返回 abcd
string.byte("ABCD") --> 返回 68
string.byte("ABCD", 4) --> 返回 65
string.byte("ABCD", 2, 4) --> 返回 66 67 68

string.len(s)

计算长度 . 返回参数字符串s的长度 .

1
string.len("qwer")  --> 返回 4

string.rep(str, n) string.rep(s, n, [sep])

拷贝字符串 . 返回字符串sn个拷贝, 以字符串sep为分隔符 . sep默认为nil(没有分隔符)

1
string.rep("abc", 2)  --> 返回 abcabc

string.sub(s, i, [j])

截取字符串 . 截取字符串s的第i到第j个字符, j缺省-1表示最后一个 , i若为负数则表示从末尾第-i个字符开始 .

1
string.sub("qwerty", -2)  --> 返回 ty (-2表示截取末尾两个)

string.match(str, pattern, [init]) string.match(s, pattern, [init])

查找配对 . 返回s字符串的第一个符合pattern描述的子集的 , init表示查找的起点 , 缺省为1 .

1
print(string.match("Hello Lua user", "%a+", 2)) --> 返回 ello

string.gmatch(str, pattern) string.gmatch(s, pattern, [init])

迭代查找配对 . 每调用一次 , 返回s字符串的下一个符合pattern描述的子集的 迭代器 , init表示查找的起点 , 缺省为1 .

1
2
3
4
5
for word in string.gmatch("Hello Lua user", "%a+") do print(word) end
--[[输出
Hello
Lua
user]]

字符串格式化

string.format(s, ...)方法使用类似C中printf()的格式化方法 , 其占位符也与C中的差不多 .

字符串格式化的占位符包括 :

占位符 含义
%c 字符 . 接受一个数字 , 并转为ASCII码对应的字符
%d / %i 有符号整数 . 接受一个数字 , 并转为有符号整数
%u 无符号整数 . 接受一个数字 , 并转为无符号整数
%o 八进制数 . 接受一个数字 , 并转为八进制数
%x / %X 十六进制数 . 接受一个数字 , 并转为十六进制数 . 前者用小写字母 , 后者用大写字母
%e / %E 科学记数法 . 接受一个数字 , 并转为科学计数法 . 前者用小写字母 , 后者用大写字母
%f 浮点数 . 接受一个数字 , 并转为浮点数
%g / %G 科学计数法/浮点数中简短者 . 接受一个数字 , 并转为%e/%E及%f中简短的一种格式
%s 字符串 . 接受一个字符串 , 并按照给定的参数格式化该字符串
%q 安全字符串 . 接受一个字符串 , 并转为克被Lua编译器安全读入的格式

占位符的进一步细化 , 在%后添加参数 :

  • 符号 : 一个 + 表示显示正数的正号
  • 占位符 : 一个 0 表示该字串至少占用的位置
  • 对齐标识 : 一个 - 表示该字串左对齐 , 默认右对齐 . 使用这个的前提是指定了字串宽度
  • 宽度数值 : 一个 数字 表示该字串的显示宽度(不足补空格)
  • 小数位数/字串裁切 : 一个 .数字 表示该字串显示的长度 , 若为浮点数则表示保留的小数位数

匹配模式

在方法string.find(), string.gmatch(), string.gsub(), string.match()中 , 可以使用匹配字串 .
Lua中的匹配字串直接用常规的字符串描述 , 匹配字串方便我们筛选需要的字符串 .

Lua支持的所有字符类

字符类 描述
单个字符 单个字符 . 与该字符自身配对 , 但 ^ $ ( ) % . [ ] * + - ? 除外
. 一个字符 . 与任何字符配对
%a 一个字母 . 与任何字母配对
%c 一个控制字符 . 与任何控制字符配对 (如 \n )
%d 一个数字 . 与任何数字配对
%l 一个小写字母 . 与任何小写字母配对
%u 一个大写字母 . 与任何大写字母配对
%w 一个数字或字母 . 与任何数字或字母配对
%p 一个标点 . 与任何标点配对
%s 一个空白 . 与空白字符配对
%x 一个十六进制数 . 与任意十六进制数配对
%z 一个0 . 与任何代表0的字符配对
%(^$()%.[]*+-?) 一个特殊字符 . 与(^$()%.[]*+-?)这些字符自身配对
[数个字符类] 括号中的任意值 . 例如[%l_]与一个小写字母或一个_配对
[^数个字符类] 非括号中的任意值 . 例如[%l_]与一个小写字母或一个_除外的字符配对

匹配模式的进一步细化 , 模式条目 :

  • 单个字符类匹配该类别中的任意单个字符 ;
  • 单个字符类跟一个 * , 表示匹配0个或多个的该类字符 , 优先匹配最长的 ;
  • 单个字符类跟一个 + , 表示匹配1个或多个的该类字符 , 优先匹配最长的 ;
  • 单个字符类跟一个 - , 表示匹配0个或多个的该类字符 , 优先匹配最短的 ;
  • 单个字符类跟一个 ? , 表示匹配0个或1个的该类字符 , 优先匹配1个的 ;

循环

Lua提供了4种循环方式 , 与其他语言的循环基本一样 , 只是语法上有一些区别 :

循环类型 描述
while 每次循环前检查条件 , 条件为true时继续执行循环 , 条件为false时退出循环
for 由for的控制语句决定是否继续执行循环体
repeat-until 重复执行循环 , 直到条件为真时退出循环
嵌套循环 各种循环体可用嵌套使用

Lua提供了2种循环控制语句 , 与其他语言的控制语句基本一样 , 但是Lua没有continue :

控制语句 描述
break 退出当前循环
goto 将程序的控制点转移到一个标签处

while循环

语法 : while(条件表达式) do 循环体 end
其中条件语句可以使用()包围 , 也可以不使用 .

数值for循环 (numerical)

语法 : for <var> = <strat>, <end>, [<step>] do 循环体 end
<var> : 循环变量 .
<strat> <end> : 循环变量的遍历区间 , 是一个闭区间 (循环变量的值在两端时都会进入循环体)
<step> : 循环变量遍历的步长 , 缺省值为1 .

for循环中的三个表达式在循环开始前一次性求值 , 之后便不再求值 .
因此若表达式中出现的变量在循环体中被修改了 , 也不会影响到循环体 .

<strat> <end>表示的是一个闭区间[strat, end] , 因此也可以直接使用一个闭区间代替 .

泛型for循环 (generic)

语法 : for <var> = in <迭代函数>, [<状态常量>], [<控制变量>] do 循环体 end
<var> : 循环变量列表 ;
<迭代函数> : 用于遍历集合 , 后面的状态常量和控制变量会自动传给迭代函数 ;
<状态常量> : 用于控制循环 , 缺省nil , 通常不指定由迭代函数指定 ;
<控制变量> : 控制变量初始值 , 用于控制循环 , 缺省nil , 通常不指定由迭代函数指定 ;
若迭代函数的第一个返回值为nil则退出循环 , 否则将第一个返回值赋值给控制变量 .

repeat-until循环

语法 : repeat 循环体 until(条件表达式)
其中条件语句可以使用()包围 , 也可以不使用 .

Lua中的repeat-until循环与C/C++中的do-while循环类似 ,
二者的区别在于 : 前者的条件表达式达成时退出循环 , 后者的条件表达式达成时继续循环 .

break语句

Lua中的break语句与C/C++的break语句使用上无异 , 其用于在循环中跳出循环 .

goto语句

goto语句用于跳转到标记处 , 分为设置标记 与 跳转标记两步 .
跳转标记 : :: 标记名 :: 设置标记 : goto 标记名

使用goto语句可用实现continue的功能 :
在循环体末尾处设置 ::continue:: 标记 , 在需要continue处使用 goto continue 语句.

示例 (Code/loop-cond-func/loopTest.lua)

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
---------- while ----------
print("while--------")
a = 1
while(a < 4) do
print("a:", a)
a = a + 1
end
---------- for ----------
print("for----------")
x = 3
for i = 1, x do
x = x - 1
i = i + 1
print(x, i)
end
---------- repeat-until ----------
print("repeat-until-")
a = 1
repeat
print("a:", a)
a = a + 1
if a == 3 then
break
end
until(a > 3)
---------- goto ----------
print("goto---------")
for i = 1, 10, 2 do
if i <= 8 then
goto continue
end
print("i:", i)
::continue::
end

输出

3.loopTest.lua输出.png


分支

Lua的分支语句与C/C++差不多 , 都是if-elseif-else , 区别只在于语法 .
Lua不提供swtich-case语句 .

if-else 语句

语法 : if 条件1 then 执行语句1 else if 条件2 then 执行语句2 else 执行语句3 end
其中条件语句可以使用()包围 , 也可以不使用 .

示例 (Code/loop-cond-func/functionTest.lua)

1
2
3
4
5
6
7
8
9
10
11
12
-- 定义方法max(a,b)返回两数中较大者
function max(a, b)
if a > b then
return a
elseif a < b then
return b
else
return b
end
end
-- 调用函数max(a,b),传入50,100. 并将结果输出.
print(max(50, 100))

输出

3.condTest.lua输出.png


方法 - 关键字function

定义语法 : [local] function 方法名(参数列表) 方法体 return 返回值列表 end
Lua中的方法也是属于普通数据类型 , 因此也有全局与局部之分 , 默认是全局的 .

Lua的方法无需指定返回值类型 , 并且支持多返回值可变参数列表 .

多返回值

Lua的方法可用返回多个结果值 , 在 return 后列出返回值列表即可 , 返回值中间用 , 隔开 .

可变参数列表

Lua的方法支持可变参数列表 , 在参数列表中使用 ... 表示有可变的参数 .

  • 使用 {...} 获得参数列表 . 传入的可变参数可视为一个table {...} .
  • 使用 select('#',...) 获取参数个数 , 使用 select(n,...) 获取第n个参数后的参数个数 .
  • 使用 arg 变量可以直接获取参数列表与参数的个数 . (仅限Lua 5.2版本之前)
    arg 是一个table , 最后一个元素为参数个数 , 其他元素表示参数列表 .

示例 (Code/loop-cond-func/functionTest.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 定义方法 printNum(num) 其返回传入的参数
function printNum(num1, num2)
return num1, num2;
end
print(printNum(10, 100))
-- 定义方法 average(...) 其返回一组数的平均值
function average(num, ...)
local result = 0
local arg = {...} -- {...}获得参数列表
for k, v in pairs(arg) do
result = result + v -- Lua中不支持++ -- += -=这几个运算符
end
return result / select("#", ...) -- select("#", ...)获得参数个数
end
print(average(1,2,3,4,5))

输出

3.functionTest.lua输出.png

变量

Lua支持动态数据类型 , 定义变量无需指定诸如int等关键字 , 只需要变量名和值即可 .

Lua的变量有三种类型 :

  1. 全局变量 : 定义方式 : 变量名 = 值
  2. 局部变量 : 定义方式 : local 变量名 = 值
  3. 表中的域

全局变量

直接在Lua文件空白处定义的变量默认是全局的 , 即便是在语句块或方法里.
直接访问未初始化的全局变量不会出错 , 默认值是 nil . 若想删除一个全局变量 , 将其赋值为 nil 即可 .

局部变量 - local关键字

只有使用了local关键字定义的变量才是局部变量 , 其作用域在其所在的代码块/方法中 .

应尽可能使用局部变量 , 原因有二 :

  1. 避免命名冲突
  2. 访问局部变量的速度高于全局变量

示例 (Code/thirdLua/varTest.lua)

1
2
3
4
5
6
7
8
a = 3             -- 全局变量
local b = 4 -- 局部变量
function fooFunc()
c = 5 -- 全局变量
local d = 6 -- 局部变量
end
fooFunc()
print(a, b, c, d) --> 3 4 5 nil

输出

2.varTest.lua输出.png

赋值语句

Lua可对多个变量同时赋值 , 变量列表和值列表的各元素用逗号分开 , 赋值语句右边的值会一次赋给左边的变量 .
变量1 , 变量2 = 值1 , 值2

遇到赋值语句Lua会先计算右侧所有的值再执行赋值操作 , 所以可以利用赋值语句交换变量的值 .
变量1 , 变量2 = 变量2 , 变量1

当变量数与值数不一致时 , Lua采取以下策略 :

  1. 变量个数 > 值的个数 : 未能赋值的变量补nil
  2. 变量个数 < 值的个数 : 多余的值被忽略

示例 (Code/thirdLua/varTest2.lua)

1
2
3
4
5
6
7
8
a, b = 1, 2     -- 给多个变量赋值
print(a, b) --> 1 2
a, b = b, a -- 交换变量的值
print(a, b) --> 2 1
a, b, c = 0, 0 -- 变量个数 > 值的个数
d, e = 1, 2, 3 -- 变量个数 < 值的个数
print(a, b, c) --> 0 0 nil
print(d, e) --> 1 2

输出

2.varTest2.lua输出.png


运算符

Lua的运算符与C/C++基本一致 , 区别主要在于语法上 , 并且相对地增加一些Lua特有的运算符 .

Lua提供了以下几种运算符类型 :

  • 算术运算符 包括 : + - * / ^ -
  • 关系运算符 包括 : == ~= > < >= <=
  • 逻辑运算符 包括 : and or not
  • 其他运算符 包括 : .. #

算术运算符

操作符 描述 示例
+ 加法 10 + 20 输出 30
- 减法 10 - 20 输出 -10
* 乘法 10 * 20 输出 200
/ 除法 (除尽) 5 / 3 输出 1.6666666666667
% 取余 5 % 3 输出 2
^ 乘幂 10 ^ 2 输出 100
- 负号 -10 输出 -10

关系运算符

操作符 描述 示例
== 等于 . 相等返回true , 不等返回false 10 == 20 输出 false
~= 不等于 . 不等返回true , 相等返回false 10 ~= 20 输出 true
> 大于 . 左边大于右边返回true , 否则返回false 10 > 20 输出 false
< 小于 . 左边小于右边返回true , 否则返回false 10 < 20 输出 true
>= 大于等于 . 左边大于或等于右边返回true , 否则返回false 10 >= 20 输出 false
<= 小于等于 . 左边小于或等于右边返回true , 否则返回false 10 <= 20 输出 true

逻辑运算符

操作符 描述 示例
and 逻辑与 . 若左边为false则返回左边 , 否则返回右边 true and false 输出 false
or 逻辑或 . 若左边为true则返回左边 , 否则返回右边 true or false 输出 true
not 逻辑非 . 对逻辑运算的结果取反 not (true and false) 输出 true

其他运算符

操作符 描述 示例
.. 连接 . 连接两个字符串 “Hello” .. [[Lua]] 输出 ‘HelloLua’
# 取长 . 对于字符串 , 取其长度 ; 对于数组 , 取其元素个数 print(#”str”) 输出 3 ; print(#{4, 5}) 输出 2
: 对于table使用 # 运算符时 , 只取”用整数作为索引的最开始连续部分的最大索引“ .
因此 , 只有使用数组的时候才适合使用 # 对table进行操作 .
详见下面的例子 : (Code/thirdLua/operatorTest.lua)
1
2
3
4
5
local tab1 = {1, 2, 3}
tab1[2] = nil
print(#tab1) --> 3 , 这里 用整数作为索引的最开始连续部分是1:1 3:3 ,最大索引是3
local tab2 = {k1 = "a", 1, k2 = "1"}
print(#tab2) --> 1 , 这里 用整数作为索引的最开始连续部分是1:1 , 最大索引是1

注释

单行注释 : -- 注释内容
多行注释 : --[[ 注释内容 ]]


数据类型

Lua是动态类型语言 , 变量无需指定数据类型 , 只要赋值即可 .

Lua的基本数据类型汇总

数据类型 描述
nil . 代表一个无效值 , 可视为 null 和 false
boolean 布尔类型 . 值有 true 和 false
number 数字 . 就是double
string 字符串 . 用一对单引号或双引号包围
fuction 函数 . 由C或Lua编写的函数
userdata 自定义类型 . 表示任意存储在变量中的C数据结构
thread 线程 . 表示执行的独立线程
table . 本质是一个关联数组(associative arrays)

示例 (Code/secondLua/simpleTest.lua)

1
2
3
4
5
6
7
-- 数据类型测试
print(type("Hello world")) --> string
print(type(10.4*3)) --> number
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type)) --> function
print(type(type(X))) --> string

输出

1.simpltTest输出.png


nil (空)

nil 类型表示空值 , 若输出一个没有被赋值的变量 , 则会输出 nil .

对于全局变量和 table , nil 还有一个删除功能 : 为一个全局变量或 table 赋值 nil 相当于删除该变量 .

示例 (Code/secondLua/nilTest.lua)

1
2
3
4
5
6
7
8
9
tab1 = {key1 = "val1", key2 = "val2"}
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end
-- 使用赋值nil删除变量
tab1.key1 = nil
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end

输出

1.nilTest.lua输出.png


boolean (布尔)

在Lua中 , falsenil 表示 , 其余的值(包括0)都表示 .

示例 (Code/secondLua/booleanTest.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print(type(true))
print(type(false))
print(type(nil))
-- 测试nil的boolean值
if false or nil then
print("至少有一个是true")
else
print("false和nil都是false")
end
-- 测试0的boolean值
if 0 then
print("0是true")
else
print("0是false")
end

输出

1.booleanTest.lua输出.png


number (数字)

Lua中所有数字默认都属于double类型 , 可以在luaconf.h中修改默认类型 .


string (字符串)

可以使用一对单引号或双引号表示一行字符串 , 也可以用两个方括号[[ 字符串 ]]来表示多行字符串 .
字符串之间使用 .. 符号进行连接 . 使用 # 可对字符串计算长度 , 放在字符串前 .

string 与其他类型数据进行连接时 , Lua会尝试将其他数据类型都转换为 string .
当对 string 进行算数操作时 , Lua会尝试将其转换为 number 类型 .

示例 (Code/secondLua/stringTest.lua)

1
2
3
4
5
6
print("2" + 6)
print("2" + "6")
print("2e2" * "6")
str = "qwerty"
print(#str)
print(#"qwerty")

输出

1.stringTest.lua输出.png


table (表)

粗浅的理解可以将table视作 array 与 map 的合体 .

在Lua中 , table 通过”构造表达式”来完成 , 例如{}用来创建一个空表 , 也可以在里面添加数据初始化表 .
Lua的表不同于其他语言的数组 , 其索引从1开始 , 且不固定长度 , 且表内元素可不同类型.

table 实际上是一个”关联数组”(associative arrays) , 数组的索引可以是数字或者是字符串 .

table的索引

对于table的索引有两种方式 :

  1. table[key] – 针对所有索引类型都可用
  2. table.key – 当索引类型是字符串时的简化写法

使用 # 可对table计算元素个数 , 放在变量名前 .

示例 (Code/secondLua/tableTest.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 创建一个空table
tbl1 = {}
-- 创建一个table并初始化
tbl2 = {1, 2, 3}
-- 创建一个table , 并进行赋值操作 , 修改值操作 , 遍历输出操作
a = {1, 2, 'var'}
a.key = "value"
key = 10
a[key] = 22
a[key] = a[key] + 11
for k, v in pairs(a) do
print(k .. ":" .. v)
end

输出

1.tableTest.lua输出.png


function (函数)

在Lua中 , 函数被视为变量(第一类值) .
说明函数可以在变量之间赋值传递 , 也可以通过匿名函数(不指定函数名)的方式作为参数传递 .

示例 (Code/secondLua/functionTest.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 定义函数
function func(n)
return n
end
function printNumber(a, b, c)
print(a + b + c())
end
-- 使用函数赋值变量
func2 = func
-- 使用函数传参
printNumber(func(5), func2(5),
function ()
return 5
end
)

输出

1.functionTest.lua输出.png


thread (线程)

在Lua中 , 最主要使用的并发方式是协程(coroutine) .

携程与线程类似 , 有自己的栈 , 局部变量 , 指令指针 , 与其他携程共享全局变量等 .
区别 : 线程可同时运行多个 , 而协程同时只能运行一个 , 处于运行状态的携程只有被挂起(suspend)时才暂存 .


userdata (自定义类型)

userdata是一种用户自定义数据 , 用于表示一种由应用程序或C/C++库所创建的类型 .
可以将C/C++的任意数据类型的数据(通常是struct 或 指针)存储到Lua变量中调用 .

此系列是本人的Lua学习笔记,文中的代码均可下载,详见:https://github.com/BRabbitFan/NoteLua

Lua简介

Lua是一种轻量级 , 易于嵌入到C/C++中的脚本语言 .
Lua是动态数据类型的 , 并且拥有自动内存管理和垃圾回收的机制 .

用处

在诸如游戏开发等方面 , Lua是常用的脚本语言 .
通常使用Lua描述顶层逻辑 , 使用C++实现底层功能 .


安装配置

在Linux / MacOS中直接下载源码使用make编译即可 .
Windows中将源码拷贝到VS工程中编译 , 或直接下载编译好的可执行文件即DLL库 .

使用make编译后可得到 lua 以及 luac 两个可执行文件 , 在windows平台下还有lua.dll库 .
其中 lua 是解释器 luac 是编译器 . Lua的轻量级由此可见一斑 .

若需要可以自行配置环境变量 .


测试

Lua的文件后缀为 .lua .
可以直接使用 lua filename.lua 解释执行 , 也可以使用 luac filename.lua 编译获得out文件 .

示例 (Code/firstLua/firstLua.lua)

编写一个简单的lua文件 fitstLua.lua :
0.firstLua.lua源码.png

解释执行

使用 lua firstLua.lua 解释执行可输出 :
0.firstLua.lua解释执行.png

编译执行

使用 luac firstLua.lua 编译可得out文件 luac.out , 再使用 lua luac.out 执行可输出 :
0.firstLua.lua编译执行.png

远程库操作以GitHub为例

初始化远程库

在GitHub中 new repository 处创建远程库.
Repository name : 仓库名
Description : 仓库描述
Public/Private : 公开/私有
README file : 一个markdown文件 , 点开该库自动显示
.gitignore : 用于设置不接受版本管理的文件
license : 许可认证等
点击 Create repository 即可创建远程库.

将远程库地址保存至本地 - git remote

在远程库中可以查看仓库地址 (包括HTTPS的和SSH的) . 可在 Code 处找到 .
git remote add 别名 远程库地址 可将一个远程库地址保存至本地 , 并取一个别名
git remote -v 可查看本地保存的远程库地址及其别名
git remote rm 别名 可删除一个保存的远程库地址


将远程库的资源获取至本地库 - git clone / git pull

使用 git clone 远程库地址 即可将远程库克隆至本地库 .
使用 git pull 远程库地址 分支 即可从远程库拉取资源至本地库 .

clone 克隆

clone用于本地没有仓库时从远程克隆一个仓库 , 是从无到有的过程 .
使用clone操作将完成三件事 :

  1. 初始化本地库
  2. 将远程库完整的克隆到本地库
  3. 创建远程库的别名 (名字默认为origin)

pull 拉取

pull用于本地库和远程库不同步时更新 .
使用pull相当于做了两个操作 :

  1. fetch 抓取远程库内容 , 只更新了本地库 , 暂存区和工作区不做更新 .
  2. merge 合并远程库和本地库 .

将本地库的资源推送至远程库 - git push

git push 远程库地址 分支 可将一个分支推送至远程库

清除push的缓存

在一次push操作之后 , 本地将保留缓存 , 使得下一次push不需要输入账号密码 .
在Windows中可以在凭据管理器中删除缓存 .

当push推送至远程库的合并失败时 , 也可以pull拉取回来解决完冲突再推送

管理团队 – 允许他人push

默认状态下 , 只有仓库的创建者才可以执行push操作 . 当多人协作开发时 , 需要让开发人员都加入团队 .

邀请他人加入团队

邀请者在仓库的 Settings -> Manage access -> Invite a collaborator 中邀请他人并复制邀请链接 .
被邀请者进入链接同意邀请即可 .


远程库的复制与请求合并

复制远程库

使用 fork 操作可以复制远程库 .
进入要复制的远程库地址 , 点击 fork 即可将该远程库复制到自己的账号中 .

发送pull请求

新远程库的拥有者可以申请pull至原远程库 .
进入新远程库的地址 , 点击 Pull request -> New pull request -> Create pull request 即可发送请求 .

原远程库的拥有者可以审核pull request请求 .
进入原远程库的地址 , 点击 Pull request -> 选择请求并审核 -> Merge pull request同意 .


SSH免密登录

远程库地址有HTTPS类型与SSH类型 , 实际开发中SSH地址更常用 .

步骤总括

  1. 在本地的用户目录下 ( cd ~ ) , 使用命令 ssh-keygen -t rsa -C github的邮箱 创建公钥-私钥对 .
  2. 进入用户目录/.ssh/id_rad.pub , 复制里面的内容 .
  3. 登录GitHub -> 个人settings -> SSH and GPG keys -> New SSH key 将复制的内容粘贴并添加即可 .

配置完毕后 , 使用SSH远程库地址代替HTTPS远程库地址即可在当天电脑免密登录 .

SSH方式的优缺点

优点 : 不需要每次都进行身份验证
缺点 : 只能同时针对一个账号和一个电脑

创建本地库 - git init

通常就直接把Git本地库的仓库根目录作为工程目录

步骤总括

  1. 新建文件夹 (这个文件夹就是工作区)
  2. 在GitBash中进入该文件夹
  3. 使用 git init 操作初始化仓库

git init操作

git init操作将一个目录初始化为git仓库 , 其会在该目录下创建.git目录 .
.git中包括如下内容 :
目录截图.git目录.png


将文件提交至本地库 - git add / git commit

需要先提交到暂存区 , 再提交至本地库

步骤总括

  1. 在本地库(工作区)中新建文件
  2. 使用 git add 文件名 操作将文件提交至暂存区
  3. 使用 git commit -m "注释" 文件名 操作将暂存区的文件提交至本地库
    • -m : message , 允许为提交操作注释
    • 注释 : 对于这次提交动作的注释

只有在本地库中 , 且通过add / commit命令操作过的文件Git才会对其进行管理 .


查看工作区与暂存区的状态 - git status

使用git status命令可以查看当前状态

例子

status例子.png
上述例子中 :
On branch master 表示主分支中

状态 含义
Changes to be committed 在暂存区和工作区都存在的文件
Changes not staged for commit 被追踪但已被更新的文件
Untracked files 在工作区但没有被追踪的文件

当暂存区里没有东西时 , 将显示 No commits yet 表示没有需要提交的东西

提交至本地库的文件不会显示在Changes to be committed中
但本地库中的文件被修改也会出现在Changes not staged for commit中 , 也要重新add commit添加


查看提交日志 - git log

git log可以查看从最近开始知道最早一次的commit动作记录

例子

log例子.png
上述例子中 :
commit : 后面的字符串是这个commit动作记录的索引
Author : 执行该动作的人
Date : 执行给动作的时间
后面的字符串是执行commit动作时 -m 参数所携带的注释

更多操作

自动分页 - 空格 / B

当日志内容太多时 , 将自动分页显示.
若最后一个日志项末尾显示 : 则表示还有下一页 , 若显示 (END) 则表示是最后一个日志项了 .
使用 空格 向下翻页 , 使用 B 向上翻页 .

单行展示索引和注释 - git log –pretty=oneline

使用–pretty=oneline参数可以一行展示一个日志项 .
这种方式只会展示每个日志项的索引和注释 .

单行展示部分索引和注释 - git log –oneline

使用–oneline参数可以一行展示一个日志项 .
与–pretty=oneline不同的是它只展示索引的前7位 .

额外展示回退所需步数 - git reflog

使用git reflog操作可以在git log –oneline的基础上额外展示一个数字 ,
这个数字表示回退到该步骤需要走几步 .


查看文件的修改记录 - git blame

git blame 文件名 可以查看指定文件的历史修改记录


切换文件的版本 - git reset

使用 git reset --hard 索引 可以切换保存过的不同版本(同时清空未保存的工作区暂存区) .

例子

使用git reflog可以看到如下样式输出 :
reflog例子.png
其中HEAD->master表示当前的版本 , 现在尝试切换到d4ebf1c版本 :
reset例子.png
可见版本被切换成功.

reset的3种模式:

  • hard : 本地库 , 暂存区 , 工作区都切换版本 (两个版本的差异被删除 , 只保留切换到的的版本)
  • mixed (缺省) : 本地库 , 暂存区切换版本 , 工作区不变 (回到add之后 , commit之前的状态)
  • soft : 本地库切换版本 , 暂存区 , 工作区不变 (回到add之前的状态)

选择版本:

可以直接使用日志项(版本)的索引 , 也可以使用如下方法 :
HEAD^ : 上一个版本 , HEAD^^ 上两个版本… ;
HEAD~1 : 上一个版本 , HEAD~2 上两个版本… .


删除文件 - rm , git add , git commit

删除一个文件 , 需要删除工作区 , 暂存区 , 本地库三处的对应文件 .

步骤总括:

  1. 删除工作区的文件 : rm 文件名 删除工作区的文件 ;
  2. 将删除操作添加到暂存区 : git add 文件名 (这个文件的最新操作是删除 , 将其添加到暂存区)
  3. 将删除曹祖提交到本地库 : git commit -m "注释" 文件名 (这个文件的最新操作是删除 , 将其提交到本地库)

    这里说将操作向上提交而不是将文件向上提交 .
    意思是并不将这个文件的物理存储删掉 , 只是新版本中没有这个文件 .
    日后回退版本也可以让这个文件重新出来 .

找回被删除的文件

直接使用git reset切换到有这个文件的版本.


对比文件的差异 - git diff

对比工作区与暂存区中的文件差异

git diff 文件名 可以比对特定文件在工作区和暂存区的差异 .
git diff 可以比对所有文件在工作区和暂存区的差异 .
其中行首标注-且整行红色字体的是被删除的行 , 行首标注+且整行绿色字体的是被插入的行 .

例子

在stest.cpp的行尾添加一行文字后 使用diff查看 :
diff例子.png
这里原先尾行的1被删除后又添加 , 猜测是因为结束标识符处于尾行 , 因此添加新尾行也改动了旧尾行

对比暂存区与本地库中的文件差异

git diff 版本索引 文件名 可以比对特定文件在暂存区和本地库中特定版本的差异 .
git diff 版本索引 可以比对所有文件在暂存区和本地库中特定版本的差异 .


分支操作 - git branch / git checkout

查看分支 - git branch -v

git branch 可以查看当前库中的所有分支 .
git branch -v 可以查看所有分支及其当前的版本 .
其中一个分支前会显示 * 这表示当前所在的分支 .

创建分支 - git branch 分支名

git branch 分支名 可以创建一个指定名字的分支名 .

当创建一个新分支时 , Git将把当前所在分支的当前版本状态复制给新分支 .
即 , 创建分支的操作是从当前状态展开一个分支 .

删除分支 - git branch -d 分支名

git branch -d 分支名 可以删除一个已经创建的分支 .

删除一个分支时 , 不能处于该分支下 . 删除操作不会删除该分支的衍生分支 .

切换分支 - git checkout 分支名

git branch 分支名 可以切换到指定分支 .

重命名分支 - git -m 新分支名

git branch -m 新分支名 可以重命名一个已经有commit的分支

合并分支 -

在一个分支中 , 使用 git merge 分支名 可以将另一个分支的内容合并入该分支 .

分支冲突

合并分支时 , 若两个分支中同一个文件里的内容不一样 , 则产生分支冲突 . 此时会进入MERGING状态 .
此时查看发生冲突的文件 (会提示) 可以查看两个分支中该文件的差异 .

解决方法 :

  1. 直接在文件中修改 , 保留需要的部分 .
  2. git add 将所有冲突的文件添加入缓存区
  3. git commit -m "备注" 提交至本地库 (注意 : 这里的commit不能带文件名)

解决冲突之后 , 则合并完成 , 结束MERGING状态 .

合并分支时以目标分支的内容为准 .
两个分支相同文件内的不同内容需要人为取舍 , 但两个分支之间的文件的不同则遵从目标分支 :
如B分支合并入A分支 , 则说明A中存在B中不存在的文件删除 , B中存在A中不存在的文件保留 .

此系列是本人的Git学习笔记,完整文档可下载,详见:https://github.com/BRabbitFan/NoteGit

Git

Git是一个免费开源的 分布式 版本控制 系统.

版本控制

记录工程文件的变化 , 以便日后的查阅修订选择.

版本控制系统的分类

  1. 集中化 版本控制系统
    用一个中央服务器存储管理工程文件 . 各开发人员从服务器下载其所需的代码段 , 工作完成后向服务器提交新代码. 管理员通过服务器管理不同人员的权限 . 集中化版本控制系统存储不同版本的差异 .
    • 优点 : 系统的维护相对轻松
    • 缺点 : 服务器的单点故障将导致整个团队无法工作
    • 典型 : SVN , CVS , Perforce
  2. 分布式 版本控制系统
    客户端并不只是提取最新版本的文件快照 , 而是把代码仓库完整地镜像下来 . 同时通过压缩算法减小对存储空间的要求 . 每个客户端都相当于一个服务器 . 分布式版本管理系统存储不同版本的索引 .
    • 优点 : 不用担心中央服务器宕机的问题
    • 缺点 : 每个客户端存储空间消耗相对大 (使用压缩算法之后只大了一点点)
    • 典型 : Git , BitKeeper

Git的本地结构与代码托管中心

Git的本机结构分为三个区域

  1. 工作区 - 当前开发的工程文件
    通过 git add 命令将文件提交至暂存区
  2. 暂存区 - 将要提交还未提交的代码
    通过 git commit 命令将文件提交至本地库
  3. 本地库 - 历史版本的信息

本地库与远程库

本地库用于每人的直接开发工作 , 可以将本地库的新内容推送至远程库 , 也可以总远程库拉取新内容至本地库 .
远程库创建于代码托管中心 , 接受所有加入团队的人进行推送和拉取内容 .

团队内部协作基本流程

  1. 项目经理创建本地库 .
  2. 项目经理使用push操作将本地库内容推送至远程库 .
  3. 开发人员使用clone操作将远程库的内容下载到本地 . (自动创建本地库)
  4. 开发人员使用push操作加入团队 , 使用push操作提交新内容至远程库 .
  5. 项目经理 , 开发人员使用pull操作从远程库拉取新内容 .

跨团队协作基本流程

  1. A团队创建本地库 , 并推送至远程库 .
  2. B团队使用fork操作将A创建的远程库复制到自己的远程库中 .
  3. 双方的开发人员使用clone push pull等操作在己方的远程库下开发 .
  4. B团队使用pull request操作发出拉取请求 , A团队审核后进行merge合并操作 , 将内容合并到A的远程库中 .

代码托管中心

代码托管中心的分类

  • 局域网 : 可使用GitLab服务器作为代码托管中心 , GitLab服务器需要自己搭建
  • 公网 : 可使用GitHub , Gitee作为代码托管中心 , 不需要自己搭建服务器

分支

在版本控制的过程中 , 可以使用多条线同时推进多个任务 . 多条线指的就是多个分支 .
分支可以继续生成分支 , 多个分支也可以合并 .
当本地库创建的时 , 自动创建一个主分支 master .

创建/合并分支的时机

  1. 开发独立新功能 :
    当开发一个新的独立功能时 , 可以开辟一个新的分支 .
    当这个功能开发完成后 , 可以将该分支的最终版本合并入主分支 .
  2. 热修复(hot-fix) :
    当系统在持续运行中发现待修复的bug时(如web服务器) , 可以开辟一个新分支 .
    当在新分支中bug修正完毕后 , 将该分支的最终版本合并入主分支 .

分支的优势 :

  • 多个分支并行开发 , 互不影响 , 提高效率 .
  • 一个分支的功能开发失败 , 直接删除该分支即可 , 不会影响其他部分 .

在Git Bash中 , 简单的Linux Bash命令都可以使用 .
仓库根目录同时也是工作区 .

基础

查看版本 : git --version
清屏 : clear
设置用户名 : git config --global user.name "xxx"
设置邮箱 : git config --global user.email "xxx"
初始化仓库 : git init

本地常用

将工作区的文件提交至暂存区 : git add 文件名
将暂存区的文件提交至本地库 : git commit -m "注释" 文件名
查看本地库状态 : git status

日志相关 :

  • 查看完整日志 : git log [--pretty=oneline] [--oneline]
  • 查看日志索引和注释 : git log --pretty=oneline
  • 查看日志简短索引和注释 : git log --oneline
  • 查看日志简短索引和注释和退回所需步数 : git reflog

查看文件的修改记录 : git blame 文件名

切换版本 : git reset [模式] 索引

  • --hard 模式 : 本地库 , 暂存区 , 工作区都切换版本 (两个版本的差异被删除 , 只保留切换到的的版本)
  • --mixed 模式 (缺省) : 本地库 , 暂存区切换版本 , 工作区不变 (回到add之后 , commit之前的状态)
  • --soft 模式 : 本地库切换版本 , 暂存区 , 工作区不变 (回到add之前的状态)

删除文件 : rm 文件名 + git add 文件名 + git commit -m "注释" 文件名 (先删除工作区的 , 再向上提交操作)

对比同一个文件的差异 : git diff [版本索引] [文件名]

  • 不指定[版本索引]表示比对工作区和暂存区的差异 , 指定则表示比对暂存区与本地库的差异 .
  • 不指定文件名表示比对所有文件 , 指定则表示比对指定文件 .

分支操作

  • 查看分支列表 : git branch -v
  • 创建分支 : git branch 分支名
  • 删除分支 : git branch -d 分支名
  • 切换分支 : git checkout 分支名
  • 合并分支 : git merge 分支名 (合并的时候注意分支冲突的解决)
  • 重命名分支 : git branch -m 新分支名

远程操作

远程库地址相关

  • 保存远程库地址 : git remote add 别名 远程库地址
  • 查看远程库地址 : git remote -v
  • 删除远程库地址 : git remote rm 别名

从远程库克隆至本地 : git clone 远程库地址
从远程库拉取资源至本地库 : git fetch 远程库地址 分支
从远程库拉取资源至本地库并合并 : git pull 远程库地址 分支 (相当于 fetch + merge)
从本地库资源推送至远程库 : git push 远程库地址 分支

0%