【mackler】电脑mod完全教程

但是我们的Vector.setXY函数里面使用了全局变量Vector
这是相当危险的,因为全局变量是在外部可以随意修改的。
如图,我们执行v1=Vector拷贝了一份Vector
接着我们如果设置Vector=nil,此时v1仍然是table,并且具有x、y和setXY域
但是当我们执行v1.setXY的时候
语句Vector.x=x这就会出错了,因为此时Vector已经是nil类型了。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 21:42:00 +0800 CST  
为了能让v1在执行setXY的时候修改的是自己的x和y,而不是Vector的x和y
我们给setXY增加了一个参数self,执行的时候也变成了self.x和self.y
这时候当我们在调用v1.setXY的时候,我们把v1本身作为参数传递给self
这样执行的就是v1.x=x和v1.y=y了

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 21:45:00 +0800 CST  
刚刚我们引用了一个self参数,使得于v1自己的方法setXY,操作的是自己的数据x和y
这几乎是面向对象的一个普遍特征了。
Lua的table可以使用":"替换掉"."来省略掉这个self
我们把function Vector.setXY(self,x,y)
改成function Vector:setXY(x,y)
调用的时候也从v1.setXY(v1,10,10)
改成v1:setXY(10,10)
如图所示,此时的代码和上一楼的代码是等价的

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 21:49:00 +0800 CST  
面向对象编程里面有两个概念,一个叫类,一个叫对象。
在我们刚刚的情景中,类就是向量这种类型,对象就是具体的v1、v2这些向量。Lua中其实只有对象,没有真正的类。
而我们所做的,其实是先定义了一个对象,之后的对象拷贝原先的对象来获得新的对象。
这不是一个非常好的做法
我们知道如果你有a对象,要让另一个对象b变得和a一样,只需要
setmetatable(a,{__index=b})即可,此时所有访问a的操作全部会变成访问b的操作
这种方法和直接拷贝的区别在于共享,我们不需要为每个对象一份完整的拷贝,我们只需要一份,其他的都指向这一份。
如图,于是我们给作为类的Vector定义一个方法new,作用是生成一个访问指向自己的拷贝。
t=t or {}
是利用or的一个把戏,如果t不是nil,那么t=t
否则t={},t为nil其实就是new在调用的时候没有传递参数
setmetatable(t,self)
把self(也就是Vector)作为t的metatable
接着设置metatable的__index域指向self(也就是Vector)
最后一句,更正一下v1:setXY(10,10)
我们逐步展开,首先展开冒号等价于v1.setXY(v1,10,10)
接着展开点号相当于访问v1的metatable的__index域
等价于getmetatable(v1).__index.setXY(v1,10,10)
而由于self.__index=self(即Vector)
所以等价于Vector.setXY(v1,10,10)
虽然Vector:setXY(x,y)是这样定义的,但是仍然相当于Vector.setXY(self,x,y)
此时我们展开得到的self是v1而非Vector,这就是我们这种做法奇妙的地方。
再看Vector.setXY(v1,10,10)的执行
v1.x=x
这一句在执行的时候,因为是更新v1的域,但是由于v1的metatable并没有__newindex域,所以,会给v1自己新增一个x域赋值为10
如果我们执行的不是更新操作而是访问操作,例如
print(v1.x)
如果v1本身没有x域,Lua会访问v1的metatable的__index域,进而得到Vector.x,也就是0

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 22:29:00 +0800 CST  
上一楼说了好多,总结一下
我们把Vector这个table当作面向对象里的类
我们利用Vector:new()方法得到了一个对象v1,此时v1内其实什么都没有。
在图中第一个箭头处,如果我们输出v1.x的值,其实我们是访问的Vector.x
而v1:setXY(10,10)相当于Vector.setXY(v1,10,10)
在这个函数中,我们更新了v1的x和y域,此时v1有了自己的x和y
所以在图中第二个箭头处,我们输出v1.x的值时,输出的是v1自己的x了

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 22:34:00 +0800 CST  
类还有一个重要的属性,继承
例如我们已经有了一个2维的向量,我们想要一个3维的向量
我们只需要再增加一个z就行了
有了前面Vector
我们只要Vector3d=Vector:new{z=0}即可新定义一个类,这个类比Vector多了一个数z
(这里补充一下,Vector:new{z=0}相当于Vector:new({z=0})这是函数调用里面的一种特殊用法)
接着我们给新的类定义一个新的方法Vector3d:setXYZ(x,y,z)先执行了self:setXYZ(x,y),接着执行self.z=z
之后我们就可以用v1=Vector3d:new()来定义一个3维的矢量啦

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-14 22:45:00 +0800 CST  
下面介绍一个概念,叫环境。
什么是环境呢,当你定义了一系列全局变量,全局函数之后。你就可以使用这些全局变量的值,调用这些全局函数。
所有这些全局变量、全局函数,都被lua存放在一个_G的table里。这个_G就叫做环境。
_G也是一个全局变量,所以_G也在_G内,_G._G就是_G。
如图,我们可以把电脑mod里面当前的环境(也就是所有全局变量和函数)全部输出出来。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 11:53:00 +0800 CST  
由于_G也是一个table,所以自然可以使用我们前面提到的设置metatable的内容,重新定制我们访问全局变量的方式

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 12:08:00 +0800 CST  
除了有全局的环境,lua也可以为一个函数设置专属的环境。
使用这个函数setfenv(function/number,table)
第一个参数可以是函数或者数字,第二个参数是table
第二个参数显然就是我们要设置的新环境了。
第一个参数如果是function,那这个table就是给这个函数设置的。
如果是number,则表示新的环境使用范围
number=1的时候,表示在当前函数使用新的环境。
number=2的时候,表示在调用当前函数的函数中使用新环境,
以此类推

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 12:20:00 +0800 CST  
例如,我们设置当前的环境变量里面包含了_G域,其值为_G,后面一个_G是存放了所有全局环境的table
在setfenv之后,如果我们执行print函数,lua会从我们设置的新环境变量里找,发现没有print域,从而产生一个错误。同时,setfenv之前执行的a=1,其值也是存在原先的_G里的,在setfenv之后,新的环境里是没有a的。
所以得_G.print(_G.a)才正确

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 13:13:00 +0800 CST  
新的环境也是table类型,因此我们可以将新的环境的metatable的__index域设置为_G,这时候,当我们访问print函数和a变量时,lua首先发现新的环境中没有print和a域,于是去访问新环境的metatable的__index域,发现指向_G,然后在_G内发现了print和a,于是正确的访问了print和a

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 13:17:00 +0800 CST  
好了,lua的基础基本介绍完了,关于lua内建库,我们和computercraft的API一起讲。
电脑也是直接使用的lua的解释器,当我们启动一个电脑或者机器人的时候,都发生了什么呢,电脑mod会自动启动一个叫bios.lua的程序。
我们就来一起看看这个bios.lua都做了什么吧!

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:03:00 +0800 CST  
所有以--开头的,或者被--[[...]]--包围的部分都是注释,注释就是lua解释器会无视的部分。只是写下来帮我们理解程序的,直接跳过,来到最开始的代码。
第一段是这里,注释说是修复了lua的string.sub和string.find
lua有几个内建库,string便是其中一个,简单的说,就是_G这个table中包含了一个string域,这个string域的值是一个table,_G.string这个table里面又包含了一系列操作字符串的函数。
而上述这个string是自带的,默认情况下就存在在_G中的。我们是可以随时调用的。
现在bios.lua先声明了一个nativestringfind=string.find,于是我们调用nativestringfind就相当于调用string.find
接着重新定义了string.find和string.sub,其中需要使用string.find和string.sub的地方都替换成了nativestringfind和nativestringsub。
经过这个步骤,后面的代码中使用的string.sub和string.find都是重新定义过的了。原来的string.sub和string.find都被隔离了。
这是一种非常常见而又有效的重新封装某些内建库的方法。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:11:00 +0800 CST  
接下来又定义了一下电脑mod特有的os的函数
os和string一样,是lua的内建库,os就是operating system,操作系统的意思,但是由于电脑mod的操作系统和真实的操作系统很不一样,因此os很多内建方法都重新定义了。
os.version()这个没啥好说的了。
重点看下面两个。先说这个os.pullEventRaw,这个函数就是一句话
return coroutine.yield(sFilter)

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:14:00 +0800 CST  
coroutine通常被翻译成协程,协程是一个类似于线程的概念,但是两者又有本质区别。
有时候我们希望电脑能多做一些事情,而不是一个程序执行完成才能执行另一个,希望电脑能同时执行好几段代码。
但是单核处理器其实无法做到这一点,因为同一个时间点,在cpu里跑的只能是一串程序,于是我们降低一点要求,希望至少看起来cpu在“同时”执行多个程序。
于是就有了不同的策略
一种策略是先执行一会儿第一个程序,然后强制中断,切换到第二个程序,执行一段时间后再强制中断,切换回到第一个程序。由于计算机速度非常快,电脑就在多个程序上快速切换,看起来似乎是”同时“执行了多个程序。
另一种策略是第一个程序执行,执行到一定时候,程序自己挂起,例如在等待用户输入的时候,然后自动切换到别的程序,等有了用户输入之后,再重新唤醒切换回原来的程序。各个程序有序的执行、挂起、切换、执行。同一时间也只有一个程序在运行。
两种策略的本质区别是中断,一种是程序自己交出控制权,自动挂起,另一种是外界强制中断程序抢走控制权。
前者的具体实现有进程和线程,后者的具体实现有协程,很多异步io的程序也用了后者的策略。
那两者的优势和劣势分别是什么呢?
由于进程和线程的切换是外界强制的,所以不存在一个进程永远不退出,一直霸占cpu的情况,可以有效管理。但是需要cpu提供中断的硬件支持,同时,由于强制中断了程序,需要保存程序运行的状态,载入切换的程序,这个过程是开销非常大的,往往需要上千到上万个时钟周期。
由于其对程序的强制有效的管理,这种模式常常是用于操作系统的多任务管理。
而协程的切换是程序自发完成的,算是一种无缝的衔接,因此开销非常非常小,往往就是程序堆栈切换的开销而已,只需要三四十个时钟周期就可以完成。这个开销太小了,要知道,从内存里取一个数字都需要上千个时钟周期。但是缺点也很显然,由于没有强制的中断,如果有某个协程长时间不挂起,是没有办法停止它的。
但由于切换速度快,常常用于某些高吞吐量需求的应用内部。可以让一个协程从磁盘读取数据的时候(可能需要几万个时钟周期),让cpu不要闲置,切换到别的协程序继续工作。
扯远了。
在lua中,我们可以通过coroutine.create(function)创建一个协程,这个协程对于的代码就是这个函数,返回值就是一个协程对象。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:40:00 +0800 CST  
coroutine和string、os一样,是lua的内建库
创建一个协程之后,我们可以通过coroutine.status来查询协程的状态。
协程有3个状态"suspended"、"running"、"dead"
我们可以通过coroutine.status(co)来查询一个协程的状态。
处于suspended状态的协程是挂起的,而处于dead状态的说明协程已经运行完了。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:53:00 +0800 CST  
当我们用coroutine.create创建一个协程的时候,协程会处于挂起状态。
我们可以用coroutine.resume(co)来激活一个处于挂起状态的协程。
激活之后,控制权就转交到协程上了,直到程序退出或者挂起。

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 21:57:00 +0800 CST  
协程内部想要在适当的时候挂起,只需要执行coroutine.yield()即可。
除此之外,程序很多内建的函数也会挂起,例如io.read()读取用户输入的时候。当需要显式的挂起时,执行coroutine.yield()即可
如图,co的初始状态为"suspended",resume之后,先输出hello world,接着运行到yield就挂起,控制权回到主程序

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 22:00:00 +0800 CST  
刚刚的例子中给的函数是没有参数的,如果我们需要给协程的函数传递一些参数呢,我们可以通过resume传递
如图,我们在resume(co后面又加上了3个参数,这3个参数会顺次传递给a,b,c

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 22:03:00 +0800 CST  
反过来,yield也是可以加上一些附加的参数的,这些参数会返回给resume
如图,首先定义了一个协程co,接着我们通过
coroutine.resume(co,"a","b","c")激活了co,并传递了3个参数
"a","b","c",此时控制权到co的函数内
co的函数执行了coroutine.yield(c,b,a)也就是coroutine.yield("c","b","a")
控制权返回主程序,resume函数返回4个值,第一个是true(如果协程dead了执行resume会返回false),接下来3个参数就是我们通过resume返回的"c","b","a"

——来自 MCLive


楼主 maple_in_thu  发布于 2014-10-21 22:08:00 +0800 CST  

楼主:maple_in_thu

字数:24301

发表时间:2014-10-12 21:33:00 +0800 CST

更新时间:2016-03-15 11:34:57 +0800 CST

评论数:395条评论

帖子来源:百度贴吧  访问原帖

 

热门帖子

随机列表

大家在看