10 2
Lua入门学习

了解到Lua(一种轻量级、可嵌入式的脚本语言),其实是很早之前和一些做游戏的朋友聊天的时候知道的,他们在项目中广泛地使用到这个语言。于是自己上网也看了一些lua开发中的一些特定发现,Lua 被运用的领域远不止游戏。特别最近很火的Redis和OpenResty(一个基于Nginx 与Lua 的高性能Web 平台)其内部集成了大量精良的Lua 库、第三方模块以及大多数的依赖项,开发人员使用Lua编写脚本就能非常轻松开发出高性能的服务了。

lua

若要了解一下lua在行业更深的应用,可以阅读云风的相关文章:点击这里

下面着重的还是Lua入门级别的学习总结

特性

Lua是一个动态弱类型语言,支持增量式垃圾收集策略。有内建的,与操作系统无关的协作式多线程(coroutine)支持。

  • 可以很方便的和用C/C++互相调用
  • 很简单,不涉及任何复杂的编程概念,对数据结构的描述能力强
  • 轻量级,库体积很小,只有几百K。
  • 学习很容易,开发效率高
  • 性能好, 支持协程,闭包等简化实现的手段

安装

lua的安装非常简单,直接参考下面的命令

Linux环境

curl -R -O http://www.lua.org/ftp/lua-5.3.4.tar.gz
tar zxf lua-5.3.4.tar.gz
cd lua-5.3.4
make linux test
make install

Mac环境

curl -R -O http://www.lua.org/ftp/lua-5.3.4.tar.gz
tar zxf lua-5.3.4.tar.gz
cd lua-5.3.4
make macosx test
make install

安装完成后,我们在终端里面敲入lua,接口进入lua的命令交互模式

$ lua

Lua 5.3.4  Copyright (C) 1994-2017 Lua.org, PUC-Rio
> print("go")
go

内置类型

既然是动态类型的脚本语言,这说明同一个变量可以在不同时刻指向不同类型的数据

lua的类型主要包含 字符串、函数、布尔、数字、空(nil)、表

1、lua内置函数 type 能够返回一个值或一个变量所属的类型:

testtype.lua

#!/usr/local/bin/lua
print(type("golang")) 
print(type(print))         
print(type(true))          
print(type(1.9))         
print(type(nil))          

执行输出

./testtype.lua

string
function
boolean
number
nil

2、字符串

字符串你可以用单引号,也可以用双引号,还支持C类型的转义,比如:

‘\a’ (响铃), 
‘\b’ (退格), 
‘\f’ (表单), 
‘\n’ (换行), 
‘\r’ (回车), 
‘\t’ (横向制表), 
‘\v’ (纵向制表), 
‘\\’ (反斜杠), 
‘\”‘ (双引号), 
‘\” (单引号)

下面的四种方式定义了完全相同的字符串(其中的两个中括号可以用于定义有换行的字符串)

#!/usr/local/bin/lua
a = 'go\n1.9"'
print(a)  
a = "alo\n1.9\""
print(a)  
a = [[go
1.9"]]    
print(a) 

使用前缀运算符’#’(也称为长度运算符)可以得到字符串的长度。

#!/usr/local/bin/lua
a = "golang"
print(#a)            
print(#"123\0xyz") 

输出:
6
7

3、数值

Lua 默认只有一种 number 类型:double(双精度)

Lua没有整数类型

#!/usr/local/bin/lua
a = 10
b = 20
c = (a+b)/2
print(c)
print(type(c))

输出

15.0
number

4、空值

nil是单值类型,即空值,其主要属性就是区别于其他任何值。Lua用空值来表示无值,表示一个值不存在。我们已经知道,一个全局变量在首次赋值之前,其默认值为空值,所以你可以将一个全局变量赋值为nil来删除该变量。

5、布尔类型

布尔值有两个值:false和true,表示通常意义的布尔值。但是,布尔值不是唯一的条件值:在Lua中,任何值都代表了一个条件。条件判断测试(如控制结构中的条件)将布尔值false和空值(nil)计为false,其他任何值均计为true。

Lua在条件判断测试中将零(0)和空字符串(“”)计为true。

#!/usr/local/bin/lua
a = 0
if a then
    print("yes")
end

b = ""
if b then
    print("yes")
end

结果输出都是 yes

6、表

Lua表分为数组和散列表,其中数组部分不像其它语言那样从0开始作为第一个索引,而是从1开始。散列部分其实就是一个Key Value的数据结构,相当于Go里面的map或者python里面的dict。

使用Lua表,可以模拟出其它各种数据结构:数组、链表、树等。

book = {name="go in action", price=256, desc="programming language book"}

可以直接操作book表

book.name = "go web programming"
book.price = "100"

你也可以定义成不同的类型的数组,比如

数组是表的一种表现形式:

books = {10,20,30,40,50}

这样看上去就像数组了。但其实其等价于:

books = {[1]=10, [2]=20, [3]=30, [4]=40, [5]=50}

所以,你也可以定义成不同的类型的数组,比如:

books = {"string", 100, "golang", function() print("golang.org") end}

注:其中的函数可以这样调用:arr[4]()

我们可以看到Lua的下标不是从0开始的,是从1开始的

for i=1, #books do
    print(books[i])
end

控制流

while循环

sum = 0
num = 1
while num <= 100 do
    sum = sum + num
    num = num + 1
end
print("sum =",sum)

if-else分支

a = 50
if a ==1  then
    print("start")
elseif a ~= 100 then
    print("half")
else
    print("finish")
end

另外,条件表达式中的与或非为分是:and, or, not关键字。

for 循环

从1加到100

sum = 0
for i = 1, 100 do
    sum = sum + i
end

until循环

sum = 2
repeat
   sum = sum ^ 2
   print(sum)
until sum >1000

结构

先简单看一个自定义的Person结构

在 Lua 中,我们可以使用表和函数实现面向对象。将函数和相关的数据放置于同一个表中就形成了一个对象。

person.lua

#!/usr/local/bin/lua

local _Person = {}

local mt = { __index = _Person }

function _Person.func1 (self, v)
    self.cardno = self.cardno + v
end

function _Person.func2 (self, v)
    if self.cardno > v then
        self.cardno = self.cardno - v
    else
        error("insufficient funds")
    end
end

function _Person.new (self, cardno)
    cardno = cardno or 0
    return setmetatable({cardno = cardno}, mt)
end

return _Person

上面这段代码 “setmetatable({cardno = cardno}, mt)“,其中 mt 代表 { __index = _M },setmetatable 将 _M 作为新建表的原型(protype),所以在自己的表内找不到 ‘func1’、’func2’ 这些方法和变量的时候,便会到 __index 所指定的 _M 类型中去寻找

创建引用测试文件 demo.lua

#!/usr/local/bin/lua
local person = require("person")
local a = person:new()
a:func1(100)

local b = person:new()
b:func1(50)

print(a.cardno)  
print(b.cardno)

输出

100
50

函数

跟go一样,函数在Lua中属于一等公民(程序将函数存在变量中,把函数作为参数传递给其他函数,返回函数作为返回结果)这些功能使语言有很大的灵活性;程序可以对函数进行重新定义,来增加新功能,或者在运行一段不信任的代码(比如从网上得到的代码)时简单地删除一个函数,从而创造安全的环境。

Lua可以调用在Lua里写的函数,以及在C语言里的函数。一般使用C语言函数,既可以实现更好的性能,还可以使用从Lua中难以直接方便地访问的功能,比如系统操作的硬件。所有的Lua标准库都是写在C语言里的,其中包括的函数用于字符串操作、表操作、I/O、访问操作系统设备、科学计算函数以及调试

定义

Lua 使用关键字 function 定义函数,语法如下

function function_name (arc)
   -- body
end

arc 表示参数列表,函数的参数列表可以为空

上面的语法定义了一个全局函数,名为 function_name. 全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于

function_name = function (arc)
  -- body
end

这种定义方式跟我们平常写javascript很像

由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”

lua中的全局变量使用需谨慎,可以使用 lua-releng-tool 脚本check非预期的global variable,尽量使用local 申明变量

local function function_name (arc)
  -- body
end

由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前

local function max(a, b)  
   local temp = nil  
   if(a > b) then
      temp = a
   else
      temp = b
   end
   return temp          
end

local m = max(-12, 20)  
print(m)

如果参数列表为空,必须使用 () 表明是函数调用。

local function func()
    print("no parameter")
end

func()

由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如

function foo.bar(a, b, c)
    -- body ...
end

此时我们是把一个函数类型的值赋给了 foo 表的 bar 字段。换言之,上面的定义等价于

foo.bar = function (a, b, c)
    print(a, b, c)
end

对于此种形式的函数定义,不能再使用 local 修饰符了,因为不存在定义新的局部变量了。

传参

Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的

local function swap(a, b)
   local temp = a
   a = b
   b = temp
   print(a, b)
end

local x = "hello"
local y = 20
print(x, y)
swap(x, y)    
print(x, y)

调用swap函数后,x和y的值并没有交换,证明函数调用中x、y是按值传递

变长参数

Lua 还支持变长参数。若形参为 ... , 表示该函数可以接收不同长度的参数。

访问参数的时候也要使用 ...

local function func( ... )

   local temp = {...}
   local ans = table.concat(temp, " ")
   print(ans)
end

func(1, 2)
func(1, 2, 3, 4)

输出:

1 2
1 2 3 4

返回值

和Go语言一样,可以一条语句上赋多个值,如:

function getBookInfo(id)
    print(id)
    return "go in action", 100, "SN123232323", "golang.org"
end
 
bookname, price, sn, website = getBookInfo()

值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。

lua编程中,经常遇到函数的定义和调用,有时候用点号调用,有时候用冒号调用,这里简单的说明一下原理。如

点号调用:

-- 点号定义和点号调用:
girl = {money = 200}

function girl.goToMarket(girl ,someMoney)
    girl.money = girl.money - someMoney
end

girl.goToMarket(girl ,100)
print(girl.money)

引用参数self:

-- 参数self指向调用者自身(类似于c++里的this 指向当前类)
girl = {money = 200}
function girl.goToMarket(self ,someMoney)
    self.money = self.money - someMoney
end

girl.goToMarket(girl, 100)
print(girl.money)

冒号调用:

-- 冒号定义和冒号调用:
girl = {money = 200}

function girl:goToMarket(someMoney)
    self.money = self.money - someMoney
end

girl:goToMarket(100)
print(girl.money)

冒号定义和冒号调用其实跟上面的效果一样,只是把第一个隐藏参数省略了,而该参数self指向调用者自身。

冒号只是起了省略第一个参数self的作用,该self指向调用者本身,并没有其他特殊的地方

一些坑

table.sort

table.sort是lua自带的排序函数,但传入的compare函数可能会出现某些问题。例如:

#!/usr/local/bin/lua
local demolist = { 9,8,3,7,0,4,5,1,5 }
table.sort(demolist,function(a,b) return a>=b end )

for i=1,#demolist do
    print(demolist[i])
end

上面代码执行的时候报错了:

invalid order function for sorting
stack traceback:
        [C]: in function 'table.sort'
        ./keng.lua:3: in main chunk
        [C]: in ?

原因在于lua的compare函数中不能出现等于(=),如果使用了=,当数组中出现两个权重一样的元素时,就会报错。具体原因你可以看table.sort的官方文档,有句话:

If comp is given, then it must be a function that receives two list elements and returns true when the first element must come before the second in the final order (so that, after the sort, i < j implies not comp(list[j],list[i]))

只能把代码改回以下的形式,结果执行就正常了

#!/usr/local/bin/lua
local demolist = { 9,8,3,7,0,4,5,1,5 }
table.sort(demolist,function(a,b) return a>b end )

for i=1,#demolist do
    print(demolist[i])
end

顺序

lua的table结构背后有一套自己的结构,这样能够保证对数据做快速的查询操作。所以key被做了hash处理,所以最后变量table的时候, key不一定都是安装预期的插入顺序进行排列的。

错误

LUA代码执行如果出现错误会停止执行函数后面的代码,会导致需要严格控制的流程只执行了一部分,产生逻辑错误。