如果是第一次学习一门编程语言,那么过程往往是比较困难的。有的语言比较容易上手,有的则比较费劲。有的语言包含的功能比较多,而有的语言又比较少。如果是第一次学习编程,当然最好是选择那些容易上手,功能比较没那么复杂的语言为好。一旦你学会了第一门编程语言,再学习第二门,难度就会下降非常多。
当你学会第二门语言,你才真正懂得一门语言
有人把各种语言分为三类:编译型、脚本型、函数型。相对来说,脚本型的语言是比较容易上手的,这类语言中比较著名的包括:PHP, Python, JavaScript, 以及本文想作为例子和大家一起学习的 Lua。
除了语言本身是否难学以外,我们还需要知道这门语言的用途。虽然现在大多数流行语言,用途都很多样,大部分应用领域在主流语言上都有使用。但是每一门语言能流行起来,肯定是历史上在某个应用领域被广泛使用过,了解这方面,对于提供学习这门语言的动力,以及学习过程中做一些例子程序,都是挺重要的。以 Lua 语言为例,它常常被用在游戏领域,作为游戏玩法逻辑的开发语言所使用,譬如一些游戏的 Mod 就可以用 Lua 来写:《饥荒》的插件、《魔兽世界》的插件。国内很多游戏公司也搭配 Lua 和 Unity 引擎来做手机游戏等等。
虽然本文用 Lua 语言作为例子来试图说明,如何学习一门编程语言的思路,但本文并不是一篇 Lua 语言的教程,因为这类的书和网站已经很多了。本文不会去完整介绍这门语言的细节,而是会去试图把一门语言要学习的内容,整理成几个“模块”。每个“模块”除了会介绍 Lua 在这个部分的一些定义以外,更重要的是介绍为什么会有这些“模块”,以便读者可以有目的性的学习。一门语言的细节非常多,一下子全部记住是不容易的,但是你如果知道了一门语言有哪些功能,一般存在于哪些“模块”中去定义,那么只需要在用的时候,去查查手册或者网上搜索一下就好了。
为什么我要选择 Lua 语言为例子来写这篇文章呢?原因是作为一门脚本语言,Lua 包含的功能和内容相对比较简洁,学习难度比较少。在游戏领域应用较多,也是一个原因。
Lua 的其他优点,包含:运行速度快,脚本解析库小,很适合用来嵌入在其他一些系统中。它的解析器本身是 C 语言写的,所以可以很好的和 C 语言代码交互,而大多数其他语言,都可以运行 C 编写的库,所以 Lua 也能比较容易的被运行起来。
任何一门语言,都会有一些最基本的语法单位组成。大部分的语言,在基本语法方面,都是极为相似的。
每一门语言学习的第一课,往往都是写一个 HelloWorld 程序。那么,我们看到一个 HelloWorld 程序的时候,除了对这么语言有个大概的印象以外,我们还需要注意什么呢?以下四个方面是我们需要注意的:
代码单元。这门语言是否对于“一段”代码有什么规定?在很多语言中,都需要把代码放在一个 main() 函数中才能运行。
print()。HelloWorld 要能打印出来,必须要有一个打印的函数,这个函数是否需要一些“引用”到本程序里的代码,表明了这门语言在“模块管理”方面的一些定义。
注释。很多程序还会带有一行或者多行的注释,可以了解注释的写法,会便于读懂很多教程上的代码例子。
运行方法。我们需要了解需要经过怎样的步骤,才能将一段程序源码在计算机中运行起来。这将是之后学习编程最常进行的操作。
我们可以看一个 Lua 的 HelloWorld 程序:
-- 本段代码存入文件 helloworld.lua
--[[
本代码用于《如何快速学会一门计算机语言》的例子
]]
print("HelloWorld")
Lua 语言没有 main() 函数。Lua 代码可以简单的一条条直接运行,而不需要用个什么东西包起来。这方面 PHP/Python 以及大部分脚本语言都是这样的。原因是脚本语言一般不需要先从一个源码文件进行编译,而是可以直接用这门语言的解析器去“运行”这个源码文件就可以了。 还有一个值得注意的是,Lua 的每行程序不需要用什么符号进行结尾,每个自然的换行就是一行代码。C 语言必须要用 ; 分号来结束一行代码,Go 语言是加分号也可以,不加也可以。
print() 函数不需要先“引用”或者“导入”什么库,说明 Lua 是有一个默认的标准库,函数直接使用就可以了。但是也要注意,自己定义函数的时候,要防止不要和这些默认的标准函数重名了。另外用双引号表示一段字符串内容,也是很多其他语言的惯例,说明 Lua 语言也可以使用这种字符串数据。
符号 -- 用来作为一行注释的开头,和很多语言用 // 或者 # 不一样,这个需要注意。而且 Lua 的多行注释,是用 --[[ 和 ]] 来包括起来的(类似 C/JAVA 的 /* 和 */ ),这个是比较特别的。原因是在 Lua 中 [[ 和 ]] 本身是用来定义一个多行的字符串常量的,所以前面加一个 -- 刚好也就是注释掉的意思。所以你不能在多行注释里面,再写一个 --[[ ... ]] ,这样两个 ]] 的范围就冲突了。
运行方法很简单: lua helloworld.lua 。在你的计算机上,你要先安装好 lua 这个解析器程序,其实就是一个命令行程序。而很多程序,都可以通过调用 lua 的 C 库,来执行一个写了 lua 源码的字符串——这些 lua 源码字符串,完全可以从文件或者网络读进去。和 Python 类似,lua 也可以用“交互式”的方法运行:不带参数的执行 lua 解析器程序,你就可以一行一行的输入 lua 代码来运行,非常适合学习。
其实通过一个 HelloWorld 的学习,已经可以掌握了这门语言相当多的内容。Lua 具备一个典型脚本语言的特征:没有 main() 函数、代码不用结束符、有默认标准库、直接解析运行。
但是如果没能理解上面这些说法也不要紧,只要把这个程序运行起来,就能开展后续的学习了。毕竟学习编程最重要的是动手操作。
编程语言最基本的能力,就是处理数据。因此数据如何在这门语言中表达,是最基本的定义。一般来说,编程语言至少要提供一些基本种类的数据的表达方法,他们包括数字、字符串、布尔值(表示 Yes 和 No)等等,有些语言对于数字还分为整数和浮点数(带小数的数字)。每一个种类的数据,一般会通过定义一种“变量类型”来代表这类数据的操作方法,一般包括以下一些定义:
定义有些什么样的值,譬如最大值最小值;以及可以用什么样的代码写法(常量)来表示这些值
定义了支持什么样的运算符,譬如加减乘除、与或非、连接和数数量、左移右移取反等等;
定义了用多大的内存以及何种二进制编码格式来存储。但这方面内容一般不需要开发者关心,除了……C语言。
这些数据,计算机语言都需要用一些变量来装载它们,才能真正的用于编程,因为变量往往代表了内存,数据在内存中才能被计算机处理。不同的编程语言,会把变量设计成“强类型”或者“弱类型”:
Lua 就是“弱类型”的语言,因此我们不需要定义变量的类型。但是数据(值)还是有类型的,或者你可以理解变量的类型,就是它的值的类型。以下是 Lua 中基本的值的类型。
数字。Lua 中不分整数和浮点数,数字常量的写法支持常见的十进制数、十六进制数、小数、科学计数法。如: num = 3.1415926 或者 num = 0xFE 。Lua 中的数字的写法非常的自由,基本上没有太多需要记忆的东西。
字符串。用双引号或者单引号都可以,如果字符串本身里面有双引号,那么就用单引号来定义,非常方便。还可以用 [[ 和 ]] 来定义多行字符串(类似的 Python 也有),比较方便直接拿 Lua 源码来记录数据。有些程序就直接用 Lua 源文件作为配置文件用。和其他很多语言类似,你可以用 \n \x2E 这些写法来输入一些键盘没法输入的字符,每个 \ 输入一个字节的内容。
布尔值。很简单,就是 true 和 false 。Lua 大小写区分,所以记得全小写。
空。这个类型很特别,只有一个值: nil 。所有 Lua 变量在赋值之前,都是 nil 值,判断一个变量有没有被用过,就是是判断是否 nil。在 if 等条件判断语句里,nil 和 false 是等价的。
有了各种类型的值和变量后,这些变量和值就可以和运算符组合在一起进行计算。不同类型的值使用的运算符也有所不同:
数字运算符:基本上就是常见的加减乘除 + - * / ,取余 % ,幂运算 ^ ,这几个和大部分其他语言都类似,还有一个整除 // ,因为 Lua 没有分整数和小数,所以需要这个专门的运算符。另外条件运算符等于、大于、小于、大于等于、小于等于 == > < >= <= 这些也很常见,要注意的是“不等于”符是 ~= ,这个和其他语言不太一样。
字符串运算符:就三个,一个是连接 .. ,别以为是 + 了,专门为了和数字区分开;另外一个是 # ,用来返回字符串长度,这个长度是按字节长度算的,如果存的是 GBK 编码的中文,一个汉字的长度是 2,如果是 UTF-8 编码中文,一个汉字的长度是 3。所以这个东西对于中英文混排的字符串,不是很适合计算多少个“字”,只能算个内存占用长度。第三个 == 也是可以用于判断字符串是否相等的。
布尔运算符: and or not 三个用文字来写的布尔运算符,含义也很清晰。有些语言会用一些符号而不是文字,我觉得还是用文字来的比较清晰一点。
空类型值 nil 的运算符,常见的就是比较的 == 和 ~= 了,其实可以完全不用,因为它就是 false 的意思,用 and or not 直接判断即可。
以上就是 Lua 语言最基本的数据类型和变量的定义了,这些和 PHP/Python/JavaScript 以及其他语言都是类似的。只需要注意一些不一样的地方,譬如:
“不等于” ~=
空值写成 nil
有个专门的整除运算 //
字符串连接用 ..
获得字符串的长度用 #
所有的计算机语言几乎都支持顺序、分支、循环这三种运行代码的流程控制。从代码的直观角度来看,这部分的关键字更容易被看见。
基本上就是从上往下一行行的运行,还没见过从下往上运行的,虽然理论上也是可以的。Lua 每个回车就算一行代码,不需要用分号结尾。
所谓分支流程,就是按照一个表达式,决定运行两段代码的其中一个。这就需要一个“判断表达式”和两端代码的写法。Lua 使用 if ... then 中的代码作为表达式,计算出 true 或者 false,用于决定后面两个代码段。这两个代码段用 then else end 区分开。如果你想要在表达式上加上括号 (...) 其实也可以,只不过这对括号其实是数学的优先级运算符号而已。至于代码段的划分,C/JAVA 等语言普遍用大括号 {...} ,Lua 就没有遵循这种传统,而是用的 then...else...end 的文字标识符,似乎这样代码可读性好一点。Pascal 语言中的代码段是用 begin...end 代替大括号来标注的,Lua 则只有 end 没有 begin ,是比较特别的。
if num > 2 then
print("num>2")
else
print("num<=2")
end
和分支类似,循环也是需要一个表达式来判断循环是否继续,然后标注一段代码重复运行。Lua 的循环有三种写法,这和很多其他语言类似。虽然这三种写法,实际上是可以互相“转换”的。不过不同的写法都适应了一些对应的循环场景。由于循环的代码比较容易让人迷糊,所以根据不同场景选择不同的写法,能够减轻一些对代码理解的负担。
a=10
while( a < 20 )
do
print("a 的值为:", a)
a = a+1
end
-- 运行 10 次的循环
while(...) do ... end 的循环非常简单,就是根据括号里表达式的计算,决定是否运行 do...end 里面的代码。和 if 有点像,只不过 then 换成了 do 作为代码段的开始标志。从这里看 Lua 还真的很希望自己的代码能接近人类语言呢。
当然在循环体代码内部,也可以加入单独的 break 语句直接结束结束循环,或者使用 continue 语句跳过本次循环,继续下次循环。一般来说这两个语句都要用 if 进行控制。下面这个里就只运行了 5 次循环就退出了
a = 10
while( a < 20 )
do
print("a 的值为:", a)
a=a+1
if( a > 15)
then
break
end
end
continue 的例子如下,循环会运行 10 次,但是前 5 次不会打印输出。如果你不用喜欢 continue,也可以放到 if...then...end 里面去实现,只不过用 continue 的话,读起代码来,意思就是:符合这个条件的话,本次循环就结束了,后面的代码被跳过。——可能更好理解一点。
a = 10
while( a < 20 )
do
a=a+1
if( a > 15)
then
continue
end
print("a 的值为:", a)
end
这种写法一般用于对数组等一批数据进行遍历处理。 for 后面的表达式,一般都是直接指定了循环的次数,并且在每次循环中可以获得一个“循环变量”,这个变量的内容随着循环的运行呈规律性变化。Lua 同样有这样的语法:
for i=0,9,1 do
print(i)
end
上面的例子会运行 10 次循环,输出从 0 到 9 的九行数字。这里用逗号隔开了三个部分,第一个部分 i=0 表示在循环体中,可以使用的循环变量为 i,初始值为 0;第二部分 ,9 表示如果 i 的值到达 9 以外的话,循环结束;第三部分 ,1 是可选的,表示每次循环结束,给 i 加上 1,当然你也可以用 -1 表示减少到某个值。虽然很多语言都 for 循环,但是 Lua 的 for 循环还是比较特别的,它执行的一个“某变量变化到某个值”的逻辑。所以这个循环变量必须是数字,而很多其他语言可以随便选,因为那些语言判断循环结束是根据一个逻辑表达式,如大于、小于这种。而 Lua 的 for 循环,并不会每次循环都计算一下 for 后面的某个部分,而是一开始就一次性定死了运行的次数,以及每次运行循环的循环变量的值。从某种意义上来说更死板,但是也更不容易出错,这可以让 for 循环更接近其设计初衷:遍历某个固定长度的数据集合。Lua 的 for 为了遍历数据集合,还有另外一种写法:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
1 one
2 two
3 three
这个例子中我们看见是对一个“数组”的遍历,通过 for 。。。 in ipairs() 的写法,每次遍历提取这个数组的索引 i 和内容 v ,是很方便的写法。
Lua 的索引是从 1 开始的,不再是从 0 开始数数了!
第三种就是 repeat ... until 循环
这种写法其实和 while ... do 循环几乎一样,只不过循环条件判断,是在循环体代码段运行之后,也就是说,这个循环体代码,至少会运行一次。需要注意的是,unitl() 里面的表达式是退出条件,而非继续循环的条件, until(false) 会一致循环下去。下面的例子循环体会运行 6 次:
--[ 变量定义 --]
a = 10
--[ 执行循环 --]
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )
除了用来写 “至少运行一次” 的循环,这个写法我们还可以用来做一些简单的“短路”逻辑。譬如我们有一个功能,需要先判断多个条件,要通过这些所有的判断后,才执行最后的操作。如果我们用 if...then...else 来写,会嵌套很多层。但是用 repeat...until 来写就会简单很多:
-- 假设我们需要判断这个变量 count
count = 10
result = 0
repeat
-- 判断是否为 0,因为后面要除这个数
if count == 0 then
break
end
-- 判断是否为负数,我们不想处理负数
if count < 0 then
break
end
-- 计算除了多少份
result = 1000/count
until(true) -- 立刻退出循环
-- 打印结果
print(result)
上面这个代码,写了两个 if 判断,用简单的 break 就可以“跳过”最后的处理。由于 until(false) 导致只能运行一次,所以也不会真正的成为“循环”。
流程控制是每一门语言都必须有的部分,这三种流程控制的语义,几乎能完成所有的功能。分支流程控制对于绝大多数语言来说,都是类似的。循环的三种写法:标准 while、遍历 for、至少循环一次 repeat,也是大多数语言都有对应的写法。所以这个部分基本上主要是注意一下关键字有没有什么特别的就好。Lua 在这里还有一个特别之处,就是关于“代码段”的标记,不是好像 C 语言那些使用 {...} 大括号,也不是 Python 这种用缩进,而是根据流程控制的语句,使用不同的“开始标志”: then/do/repeat ,以及使用“结束标志”: end/until() 。
函数一般来说是代码的可复用的容器,我们可以把一块代码“放在”函数里面,方便反复运行。很多语言函数都是最重要甚至是唯一的代码“容器”。对于 Lua 来说,虽然我们不需要把所有代码都放到某个函数里面,但是我们依然可以定义函数。函数一般包含两方面的内容:
--[[ 函数返回两个值的最大值 --]]
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end
-- 调用函数
print("两值比较最大值为 ",max(10,4))
print("两值比较最大值为 ",max(5,6))
对于 Lua 来说,定义一个函数,需要用关键字 function 作为开头,然后跟着名字、参数列表 name(param) ,然后紧跟着下一行就是函数代码段了。这里不像其他的语言有一个代码段的“开始标志”,而是直接就开始了。最后函数代码段以关键字 end 结尾。由于 Lua 是一门“弱类型”语言,所以函数的参数也不需要写类型,只要把参数名字写上就好了。同样的,Lua 的返回值也不需要把类型声明到 function 那一行,直接在函数体里面用 return 去返回就好了。
对于函数来说,Lua 还支持两个特别的写法:可变参数、多返回值。这两个写法在某些情况下,能很方便的编写出容易使用的函数。
所谓“可变”参数,意思是这个函数的参数个数是不确定的。这种设计很方便开发者用来输入多个数据,然后集中处理。C语言里面著名的 printf() 函数就是一个可变参数函数。同样 Lua 也可以这么做。说起来,好像大多数的语言都有这个能力呢。
function average(...)
result = 0
local arg={...} --> arg 为一个 table 类型变量,把所有参数都装进去
for i,v in ipairs(arg) do --> 这里进行所有参数的遍历
result = result + v
end
print("总共传入 " .. #arg .. " 个数")
return result/#arg --> #arg 返回这个表的成员个数
end
print("平均值为",average(10,5,3,4,5,6))
上面的例子中,我们用了 Lua 的遍历一个 Table 这种数据集合(后续会专门介绍)的写法: for...in ipairs() ,而被遍历的对象,就是可变参数,虽然它看起来只有三个点 ... 。虽然大部分情况下,我们通过 {...} 都可以把可变参数作为一个 Table 来使用,但是 Lua 也提供了几种特别的使用 ... 的写法:
可以用来把全部的可变参数,以 ... 的写法直接调用另外一个可变参数函数,如 string.format(fmt, ...)
select('#', …) 返回可变参数的长度
select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表。
大部分语言的函数返回值,都只能有一个。但是一个函数的功能,可能会不止返回一个数据结果,譬如最常见的是,需要返回一个结果和一个错误码——很多函数运行过程中都需要把出现的各种错误(异常)情况返回给调用者。所以每一种不支持多个返回值的语言,几乎都有一套自己的玩法来做这个事情。譬如 C 语言,就常常把返回值设计成 int,如果返回 0 表示成功,其他值表示失败,这样用 if(some_fun()){...} 的写法就可以直接写错误异常处理了,但是这种情况下就需要设计所谓“输出参数”,让使用者直接先准备一个存放结果的变量,通过参数传入这个函数。而 Java 语言则旗帜鲜明的反对“输出参数”,一来是 Java 不需要程序员自行管理内存,二来是 Java 有“异常”机制:函数可以返回一个对象作为返回值,并且可以抛出一个异常,来表示函数运行错误。Go 语言是支持多返回值的,这个事情就变得简单了,很多函数都会返回两个返回值,一个是正常的结果,另外一个是 error 类型的错误变量,通过判断这个错误返回值是否 nil,就可以进行错误处理。
对于 Lua 来说,由于支持多返回值,你可以学习 Go 的传统,让每个函数都有最少两个返回值,也可以用其他的方法来编写函数。但是至少你有多返回值这个能力,很多情况下都是很方便编程的。
-- 一个会返回“除0错”的除法函数
function devide(a, b)
if b == 0 then
return 0, "Division by zero"
end
return a/b, "OK"
end
-- 调用函数
result,err = devide(10, 0)
if err ~= "OK" then
print("Error:", err)
else
print("Result:", result)
end
这个例子我用了一个字符串来代表错误,其实是不够严谨的,但这只是一个例子。你可以用一个数字来表示错误码,然后建立一个错误码的 Table 来组织所有的错误码提示文字。
看到这里,我们可以发现 Lua 真的是很不喜欢括号呢——不但不用大括号作为代码段的标记,也很少使用小括号用来做分支、循环判断表达式的标记,只在函数的参数列表上用了括号,估计这也是为了遵循“函数”这个数学传统。这一点和 Python 真的有点像,但又没有 Python 那样“强迫症”一样要求开发者进行代码缩进对齐。
如果了解了以上的内容,其实已经基本可以用 Lua 来做一些简单的事情,譬如编写配置文件。这一部分实际上和很多其他语言,都非常的相似。但是我们还有一个很重要的内容,也是 Lua 的其中一个特色内容没有涉及,就是 Lua 自带的一种通用数据结构:Table。下一篇我们会从 Table 入手,逐步探索编程语言的一些高级语法。
评论区
共 24 条评论热门最新