关于LUA(下)
Lua与OOP
Lua是面向过程的语言,不提供面向对象的特性,但是我们可以利用Lua的元表和元方法模拟面向对象的效果。
OOP的特性
封装
所谓封装,是隐藏对象的属性和细节,仅对外暴露公共的访问方式。本质上分为两层:
1)成员变量和成员方法,提升代码的内聚性,降低模块间耦合
2)权限控制,对外暴露统一接口,对象内部不被任意篡改,且后续维护成本低(保证外部接口不变的前提下,内部实现可任意更改)
继承
所谓继承是指子类拥有父类所有的功能和属性而不必重新实现,好处在于实现代码的复用。
多态
多态分为两种,运行时多态和编译时多态:
所谓编译时多态是指函数重载,即编译时编译器根据函数调用时对象的类型和形参列表的个数以及参数类型决定实际调用的函数,意义在于提供了一种统一的接口命名规则,保证接口含义明确,代码结构整齐。
另外一种即运行时多态,即通过子类重写父类的虚函数,在运行时借助虚表,实现运行时根据指针实际指向对象的类型调用对应的函数,意义在于简化代码,对于不同类型对象可以统一用一套接口规则,在实际运行时根据指针指向对象的真实类型选择具体的动作。
Lua与类成员
在Lua中,table是一个非常灵活的结构,他的val不仅可以是字符串、数字、地址,甚至可以是函数,因此我们可以用Lua的table来描述一个类的成员方法和成员变量。写了一段示例代码如下:
Person = {} function Person:new(name,age) local person = {} setmetatable(person,{__index=self}) person.name = name person.age = age; return person; end function Person:Print() print(self.name,self.age); end local_person = Person:new("aaa","bbb") local_person:Print()
这段代码的含义很明确,我们定义了一个Person类,里面含有两个成员变量:name和age,和两个成员方法:new和Print,分别用来构造Person对象和打印Person对象里面的值,但是这里需要有几点值得关注的:
Lua中的self
首先看Lua官方的文档对self的描述
A table in Lua is an object in more than one sense. Like objects, tables have a state. Like objects, tables have an identity (a selfness) that is independent of their values; specifically, two objects (tables) with the same value are different objects, whereas an object can have different values at different times, but it is always the same object. Like objects, tables have a life cycle that is independent of who created them or where they were created. Objects have their own operations. Tables also can have operations: Account = {balance = 0} function Account.withdraw (v) Account.balance = Account.balance - v end Then, we can call it as Account.withdraw(100.00) However, the use of the global name Account inside the function is a bad programming practice. First, this function will work only for this particular object. Second, even for this particular object the function will work only as long as the object is stored in that particular global variable; if we change the name of this object, withdraw does not work any more: a = Account; Account = nil a.withdraw(100.00) -- ERROR! Such behavior violates the previous principle that objects have independent life cycles. A more flexible approach is to operate on the receiver of the operation. For that, we would have to define our method with an extra parameter, which tells the method on which object it has to operate. This parameter usually has the name self or this: function Account.withdraw (self, v) self.balance = self.balance - v end Now, when we call the method we have to specify on which object it has to operate: a1 = Account; Account = nil ... a1.withdraw(a1, 100.00) -- OK With the use of a self parameter, we can use the same method for many objects: a2 = {balance=0, withdraw = Account.withdraw} ... a2.withdraw(a2, 260.00) This use of a self parameter is a central point in any object-oriented language. Most OO languages have this mechanism partly hidden from the programmer, so that she does not have to declare this parameter (although she still can use the name self or this inside a method). Lua can also hide this parameter, using the colon operator. We can rewrite the previous method definition as function Account:withdraw (v) self.balance = self.balance - v end and the method call as a:withdraw(100.00)
总结一下是这样几个点:
1)Lua中table是一个非常丰富的对象,它有自己的状态(里面KV对的值),有自己的唯一标识,也可以定义自己的操作
2)上面举了一个例子,定义了一个Account的table,并定义了这个table的withdraw方法,并在方法里面操作“成员变量”balance,但是实际编程实践不推荐在函数里面直接用全局变量名引用这个方法,如在某些场景下(如代码修改、代码内赋值等),对象名字变了,那么程序会出问题,为此我们希望有一种更加灵活的方法将操作和对象绑定,于是我们在方法里面加一个额外参数,表示当前要操作的对象(这就是self),但是为了方便呢,Lua允许我们隐藏这个self参数,我们可以在代码中直接引用(其实和C++、java类似)
3)冒号“:”和引用号“.”的区别:注意到在普通table中,取一个table的item直接采用table-name.item即可,示例中的代码用的是类似Person:new的定义和调用机制,其实冒号":"就是省略了一个self,前面代码中function Person:Print()等同于function Person.Print(self)
4)那么Person:new能不能写成如下形式?
Person = {} function Person:new(name,age) local person = {} setmetatable(person,{__index=self}) self.name = name self.age = age; return person; end
Lua中的“成员方法”
注意到Person的成员方法定义方式如下:
Person = {} function Person:new(name,age) local person = {} setmetatable(person,{__index=self}) person.name = name person.age = age; return person; end
实现定义了一个person的局部变量,反正是类对象自己的成员变量,为何需要创建一个person,而不是直接对类成员变量赋值,如下形式:
Person = {} function Person:new(name,age) local person = {} setmetatable(person,{__index=self}) self.name = name self.age = age; return self; end
这是因为,我们定义的Person是一个全局的table,若是在new里面直接用self的话,每调用一次Person:new就变成对全局的Person的name/age进行设置/修改;(需要注意的是new方法并不是真的创建了一个对象,只是我们为了贴合C++/JAVA的习惯写的接口,若是不通过内部定义变量手动创建对象的话,本质上它并没有内存分配,都是在操作一块内存)
__index元方法的使用
通过前面介绍我们知道,__index元方法的作用是,当我们用一个key查找table找不到时,会去看有没有设置__index元方法,这个元方法可以指向的是一个function,也可以指向的是一个table;我们在new方法里面调用了setmetatable,为person对象创建了metable,并设置了__index元方法当我们:
1)调用local_person.Print()的时候,其实local_person自己并没有定义Print方法(他只有成员变量name和age,是Person调用了成员方法),这个时候去index元方法指向的table(即Person的表)去找,找到了Person方法,并传入local_person的地址给Print的self参数
2)当调用Person:new的时候,因为调用主体是Person,所以在setmetatable里面index=self,实际上是index指向了Person表。
Lua与继承
继承分为两类,一类是操作继承,一类是成员继承;在前面那一段我们实际是person继承了Person定义的方法,实现了操作继承,这里我们主要介绍成员继承;在介绍成员继承之前,我们先演示一下为啥前一段的写法无法达成成员继承的目的,如下:
Person = {score="9898"} function Person:new(name,age,score) person = {} setmetatable(person,{__index=self}) person.name = name person.age = age; self.score = score; return person; end function Person:Print() print(self.name,self.age,self.score); end local_person = Person:new("aaa","bbb","ddd") b = Person:new("aaa","bbbic","qqqq") local_person:Print() b:Print()
原因如前所示,Person是全局变量,你在接口里面改了是相当把所有self指向的Person表的状态都给改了;那么怎样写一个能正常进行属性继承和方法继承的接口呢,如下所示:
Person = {} function Person:new(name,age) person = {} setmetatable(person,{__index=self}) person.name = name; person.age = age; return person; end function Person:Print() print(self.name,self.age); end Student = {} setmetatable(Student,{__index=Person}) function Student:new(name,age,score) student = Person:new(name,age) student["score"] = score setmetatable(student,{__index=self}) return student end function Student:new_print() print(self.name,self.age,self.score) end s = Student:new("aaa","bbb","ccc") s:new_print() s:Print() s2 = Student:new("aaa","bbb","ccc2") s2:new_print() s2:Print()
分析代码可以看到:
1)定义了两个类,Person和Student,其中Student是Person类的子类
2)我们的Person和Student实际上只定义了接口(即“类”的方法部分),属性部分是在new方法中另外单独创建了一个table实现的,对应的方法访问通过__index指向进行设置
至此,类和对象之间的指向关系图如下:
Lua与多态
如前所述多态包括编译时多态和运行时多态,编译时多态主要是根据参数列表进行调用接口的区分,在Lua中也可以采取同样的方法去做;而对于运行时多态,Lua的机制天然支持,在C++中多态是靠虚表支持的,函数调用的时候,发现是虚函数去虚表里面找,子类重写了虚函数则虚表里面对应项就是子类实现的接口,否则是父类接口,而在Lua中__index查找顺序天然的满足虚机制的效果,即若想做到类似C++多态的效果,则只需要在子类重写父类的接口,在执行调用操作的时候优先找子类的table,则意味着若是子类重写了父类的接口会被优先调用到,否则调用父类的接口,如下代码所示:
Person = {} function Person:new(name,age) person = {} setmetatable(person,{__index=self}) person.name = name; person.age = age; return person; end function Person:Print() print(self.name,self.age); end Student = {} setmetatable(Student,{__index=Person}) function Student:new(name,age,score) student = Person:new(name,age) student["score"] = score setmetatable(student,{__index=self}) return student end function Student:Print() print(self.name,self.age,self.score) end function DuoTai(inst) inst:Print() end s = Student:new("aaa","bbb","ccc") s2 = Person:new("aaa","bbb") DuoTai(s) DuoTai(s2)
Student和Person都定义了Print接口,函数DuoTai接收形参并调用其Print接口,最终会调用到实际对象类型所定义的Print接口,执行效果如下图
可见达到了根据实际对象类型调用对应接口的效果。
Lua与权限控制
至此我们已经通过Lua模拟了面向对象大部分的特性,还剩一点即OOP的封装性,即在OOP中有些成员变量和成员方法,我们不希望暴露给外部,那么Lua是否可以做到这一点呢?我们先了解以下基础知识。
upvalue机制
以下是Lua官方对upvalue机制的阐述
While the registry implements global values, the upvalue mechanism implements an equivalent of C static variables, which are visible only inside a particular function. Every time you create a new C function in Lua, you can associate with it any number of upvalues; each upvalue can hold a single Lua value. Later, when the function is called, it has free access to any of its upvalues, using pseudo-indices. We call this association of a C function with its upvalues a closure. Remember that, in Lua code, a closure is a function that uses local variables from an outer function. A C closure is a C approximation to a Lua closure. One interesting fact about closures is that you can create different closures using the same function code, but with different upvalues. To see a simple example, let us create a newCounter function in C. This function is a factory function: It returns a new counter function each time it is called. Although all counters share the same C code, each one keeps its own independent counter. The factory function is like this: /* forward declaration */ static int counter (lua_State *L); int newCounter (lua_State *L) { lua_pushnumber(L, 0); lua_pushcclosure(L, &counter, 1); return 1; } The key function here is lua_pushcclosure, which creates a new closure. Its second argument is the base function (counter, in the example) and the third is the number of upvalues (1, in the example). Before creating a new closure, we must push on the stack the initial values for its upvalues. In our example, we push the number 0 as the initial value for the single upvalue. As expected, lua_pushcclosure leaves the new closure on the stack, so the closure is ready to be returned as the result of newCounter. Now, let us see the definition of counter: static int counter (lua_State *L) { double val = lua_tonumber(L, lua_upvalueindex(1)); lua_pushnumber(L, ++val); /* new value */ lua_pushvalue(L, -1); /* duplicate it */ lua_replace(L, lua_upvalueindex(1)); /* update upvalue */ return 1; /* return new value */ } Here, the key function is lua_upvalueindex (which is actually a macro), which produces the pseudo-index of an upvalue. Again, this pseudo-index is like any stack index, except that it does not live in the stack. The expression lua_upvalueindex(1) refers to the index of the first upvalue of the function. So, the lua_tonumber in function counter retrieves the current value of the first (and only) upvalue as a number. Then, function counter pushes the new value ++val, makes a copy of it, and uses one of the copies to replace the upvalue with the new value. Finally, it returns the other copy as its return value. Unlike Lua closures, C closures cannot share upvalues: Each closure has its own independent set. However, we can set the upvalues of different functions to refer to a common table, so that this table becomes a common place where those functions can share data.
总结起来以下几点:
1)upvalue机制实现了类似C里面static类型变量的效果(即生命周期和程序生命周期一致),但仅在函数内部作用域可见,
2)在Lua中每创建一个C函数对象,我们可以在这个函数对象内保留任意个upvalues,且被自由访问;这个C函数以及这些upvalue构成一个C闭包
3)对于C的闭包来说,代码一套,但是upvalues之间是独立的
示例代码说明:
1)lua_pushcclosure用于在虚拟机栈上创建一个闭包,第二个参数是C函数地址,第三个参数是upvalue的个数,我们需要在那之前把upvalue的初始值入栈
2)调用完newCounter之后,实际在虚拟机栈上留下了一个闭包对象
3)lua_upvalueindex,用于生成一个upvalue的索引,lua_replace用于将upvalue的值替换
4)函数counter的逻辑是,将下标为1的upvalue取出来,加1并入栈,用新的值替换下标为1的upvalue(lua_replace会将lua_pushvalue进去的pop出来)
闭包
先看看Lua官方对闭包的定义
When a function is written enclosed in another function, it has full access to local variables from the enclosing function; this feature is called lexical scoping. (静态作用域) Let us start with a simple example. Suppose you have a list of student names and a table that associates names to grades; you want to sort the list of names, according to their grades . You can do this task as follows: names = {"Peter", "Paul", "Mary"} grades = {Mary = 10, Paul = 7, Peter = 8} table.sort(names, function (n1, n2) return grades[n1] > grades[n2] -- compare the grades end) Now, suppose you want to create a function to do this task: function sortbygrade (names, grades) table.sort(names, function (n1, n2) return grades[n1] > grades[n2] -- compare the grades end) end The interesting point in the example is that the anonymous function given to sort accesses the parameter grades, which is local to the enclosing function sortbygrade. Inside this anonymous function, grades is neither a global variable nor a local variable. We call it an external local variable, or an upvalue. Consider the following code: function newCounter () local i = 0 return function () -- anonymous function i = i + 1 return i end, function() return i end end c1, d1 = newCounter() c2 = newCounter() print(c1(), d1()) --> 1, 1 print(c1(), d1()) --> 2, 2 print(c2()) --> 1 Now, the anonymous function uses an upvalue, i, to keep its counter. However, by the time we call the anonymous function, i is already out of scope, because the function that created that variable (newCounter) has returned. Nevertheless, Lua handles that situation correctly, using the concept of closure. Simply put, a closure is a function plus all it needs to access its upvalues correctly. If we call newCounter again, it will create a new local variable i。 Technically speaking, what is a value in Lua is the closure, not the function. The function itself is just a prototype for closures.
总结来说,以下几点:
1)当一个函数A定义在另一个函数B中,函数A对于函数B内的局部变量拥有访问权限,我们称这样的变量为external local variables或者叫upvalues;
2)Lua中闭包不仅包括function的尖括号内的执行逻辑,还包括里面的upvalues;并且闭包可以当作一个“对象”看待,且之间相互独立,说明中newCounter被创建两次,里面的局部变量i的值是互不影响的;
3)需要注意的是Lua的闭包里面,多个函数访问同一个upvalue,其值是共享的,如下
function ClosureTest () local i = 0 return function () i = i+1 return i end, function () i = i+1 return i end end c1,d1 = ClosureTest() c2,d2 = ClosureTest() print(c1(),d1()) print(c1(),d1()) print(c2()) print(d2())
两个匿名函数,对于i的改变是相通的,即每次调用i会改变两次,增2;但是每个闭包对象里面的upvalue确是相互独立的。
Lua的闭包与权限控制
因为Lua的闭包其extenal local value具备内部可用且独立&外部不可访问的特点,给编程人员提供了一种权限管理的机制,我们可以将不希望暴露的接口作为内部函数进行定义,有如下代码:
function CreateCar()
local _Car = {color="red",speed=90}
function _Car:Print()
print(self.color,self.speed)
end
local function CheckSpeed()
if (_Car.speed > 100)
then
return false
end
return true;
end
function _Car:AddSpeed()
if(CheckSpeed())
then
self.speed = self.speed + 10
end
end
return _Car;
end
Car1 = CreateCar()
Car1.AddSpeed()
Car1:Print()
Car2 = CreateCar()
Car2.AddSpeed()
Car2:Print()
上述代码段中:
1)直观上看我们定义了一个“类”,_Car,含有两个成员变量color和speed;同时有三个成员方法:Print、CheckSpeed、AddSpeed;
2)其中函数CheckSpeed被修饰为local function,则意味着CheckSpeed只能在CreateCar的代码块内可见,从而达到限制接口对外暴露的效果;
3)当我们在外部调用CreateCar时,我们可以理解为创建了一个_Car一样的表,这个表里面含有color、speed、Print、AddSpeed这几个表项
4)而CheckSpeed是CreateCar这个闭包里面的一个闭包,_Car是其一个upvalue,因此AddSpeed对于_Car的修改对于CheckSpeed是可见的,也是共享的
5)因为每次调用CreateCar之后,就独立创建了一个表,因此_Car的状态互相独立且不干扰
6)此外,对于AddSpeed来说,CheckSpeed也是他的一个upvalues
7)针对这个示例直观上,我们可以认为实际情况,如下图所示:
参考资料