注:本文代码中的->符号均为运行后的输出结果,不算做代码
Contents
前言
说道面向对象,基本就是围绕着表来展开的。lua中的表是一个万能的类型,可以当做数组,哈希表,结构体,等数据类型,主要是搭配元表来实现面向对象。前面会有些lua表的基础简单介绍,如有了解可直接翻找目录 “Lua类型与实例”,因为博主常使用C#进行开发,所以本文章中会有很多关键字与命名风格偏向C#。
Lua中的表:table
表和其他基础类型不同,它是引用类型的,或者叫指针类型。当给变量赋的值为表时,只是把表的引用复制了过去。
表的声明方式:
local tb = {}
没错,表仅仅使用{}就可以动态的创建了一个表对象,不需要任何的构造函数或new等关键字来创建。
表可以表示的数据类型
当做数组使用
array = {1, 3, 5, 7, 12}
当做字典使用
dict = {["jason"] = 63, ["jack"] = 70, ["green"] = 73}
当做枚举使用
StateType = { Unknow = 1, Open = 2, Close = 3 }
两种形式的赋值
local tb = {} tb.field = 0 --等同于 tb["field"] = 0
以上是field作为表元素被赋值,两种写法结果相同,只是表现方式不同,但是使用场景不同,从语意来讲前者可以是个枚举值,或者是个类中的字段,而后者是对字典的操作。
Lua中的函数:function
lua中的函数是第一类值,也就是被当做一种普通的类型可以储存在变量和表中,函数也是引用类型。
local func = function(i) return i * 2 end ---上下等同--- local function func(i) return i * 2 end
以上两种方法声明方式都是正确的,只是第一种写法看起来更像是匿名函数,第二种则是普通的静态方法。
储存方法的变量同样可以成为表中一个普通的字段,并且提供了语法糖,让它看起来更像一个函数。
local tb = {} tb.func = function(i) return i * 2 end -----等同于----- function tb.func(i) return i * 2 end
既然方法只是表中的一个普通元素,而元素的key都是作为字符串存在的,所以可以直接拿方法名来索引,也就是Lua中的“反射”。
tb["func"](2)
元表:metatable
元表,metatable,允许改变表的行为,lua提供了许多原方法,可以实现运算符重载、查询、更新等功能。
运算符重载
__add | 对应的运算符 '+'. |
__sub | 对应的运算符 '-' |
__mul | 对应的运算符 '*'. |
__div | 对应的运算符 '/'. |
__mod | 对应的运算符 '%'. |
__unm | 对应的运算符 '-'. |
__concat | 对应的运算符 '..'. |
__eq | 对应的运算符 '=='. |
__lt | 对应的运算符 '<'. |
__le | 对应的运算符 '<='. |
普通元方法
__call | 把table当做一个方法执行 |
__tostring | 修改输出行为 |
__newindex | 对表更新 |
__index | 对表查询 |
其中最常用的就是__index,当你给一个表A设置了一个元表B,元表B的__index指向了表C,那么当你访问表A一个不存在的元素时,那么Lua就会寻找元表中的__index,也就是所指向的C,如果存在则返回结果,不存在返回nil
local A = {} local C = {value = 3} local B = {__index = C} setmetatable(A, B) print(A.value) --结果 3
设置元表的函数:setmetatable(table, metatable),返回值table,这个返回值一般是在没提前分配表时需要的,如
local tb = setmetatable({}, {__index = C})
获取元表的函数:getmetatable(table),返回值metatable。
静态类
类使用Lua中的模块来实现,字段和方法(函数)直接作为表的元素
TestClass = {} TestClass.field1 = 0 TestClass.field2 = 1 function TestClass.method1(i) return i * 2 end function TestClass.method2() print("hello") end return TestClass
调用
TestClass = require "TestClass" print(TestClass.method1(3))
类型与实例
类型,是面向对象中对数据抽象的原型,而实例则是原型(prototype)克隆的副本。
类型就像是做好一个模板(原型),然后使用这个原型复制出很多新的实例对象。
local Human = {} ---实例化方法(克隆原型为新对象) function Human.New(name) local obj = setmetatable({}, { __index = Human} ) obj.constructor(obj, name) return obj end ---构造函数 function Human.constructor(self, name) self.name = name end function Human.Say(self) print("i am "..self.name) end return Human
一个Human类就完成了,只需要Human.New()就可以创建一个Human原型的副本(类型实例化)。
setmetatable函数内,创建了一个新表,并把本table设置为了新表的查询/索引后,返回这个新创建的表。
也就是说返回的这个表对象中是没有元素的,所以通过查询元表的__index所指向的表Say方法和name字段。
local human = Human.New("jason") human.Say(human) -> i am jason
另外__index只用于查询,无论有没有该元素,在赋值时都是对当前表对象赋值和修改,所以并不会通过对象修改原型中的成员。
human.Say(human)这个句子看起来可能有些奇怪,为什么还有传进去个human,也就是调用的对象本身呢,但实际上并不奇怪,因为对象成员方法的本质就是普通静态方法(类成员方法),只是许多面向对象的语言会隐藏掉对象方法的第一个隐藏参数,那就是this指针(self),参考https://www.imxqy.com/code/cpp/cclass.html,这篇文章会更好理解。
Lua提供了可以让我们的代码看起来更像面向对象的语法,提供了":"这个语法糖,它可以在调用时自动把调用的对象传入第一个参数,而方法名也使用:来声明,默认传入第一个参数为self。
改成如下
local Human = {} ---实例化方法(克隆原型为新对象) function Human.New(name) local obj = setmetatable({}, { __index = Human} ) obj:constructor(name) return obj end ---构造函数 function Human:constructor(name) self.name = name end function Human:Say() print("i am "..self.name) end return Human
调用
local human = Human.New("jason") human:Say() -> i am jason
如果仅用过C#和Java这类纯面向对象语言无法理解这个逻辑的话:
可以记为直接将.调用的就是静态方法,而:调用的就是实例方法(但并不推荐这么记,大多数纯Lua开发下不会有什么问题,但在做胶水语言的时候,如和C#互相调用,可能会发生些问题。比如将Lua的一个回调函数传进了C#里,而纯C#开发者如果不了解委托原理那么可能就会出现Lua回调函数中self对象丢失的问题,因为委托是储存了实例对象的函数指针)
文章评论