Lua总结

ref http://book.luaer.cn/

基础

  • 弱类型,没有类型定义
  • 解释型语言,但是会通过编译器先编译成中间码然后再执行
  • 字符串的处理:用table保存,最后table.concat(t, "\n") .. "\n"。防止..引起的性能问题
  • 注释: --
  • 块注释:—[[ ]]
  • 没有++,+=
  • 打印:print(a, b, c, “”, 1)
  • 字符串:’或者“均可
    • 或者[[包含’和”]]
  • 不需要声明类型,语句结束不需要加;
  • 赋值语句:同时赋值多个对象 a, b = c, d
    • swap: x, y = y, x==>Lua会先计算右边的值再进行操作
  • 不需要指定变量类型,有8种类型
    • 1. nil
    • 2. boolean=>true false
    • 只有nil和false为false,0和空字符串为true
    • 3. number
    • 4. string
    • 5. function
    • 6. userdata: 从C当中分配出的一块内存空间,允许C编写的函数存储和访问数据。
    • userdata不能在lua中创建和操作,只能通过C的API来使用
    • 7. thread
    • 8. table: 数组、关联数组、哈希表、set、list、tree、对象
    • type(变量名)
  • 字符串连接运算符..,而不是+
  • 变量前加local是局部变量,否则全部都是全局变量
  • 通过do...end创建代码块,相当于c++的{},并在其内创建local变量
    • do
    • ...
    • end
  • ^:指数运算符
  • ~=:不等于
  • 逻辑运算符:and or not
  • 长度运算符:#(#”123”)返回3
  • table:
    • 1. key-val形式:
    • t = {k1=v1, k2=v2}
    • t.k2 = v3
    • t.k3 = v3
    • 2. 下标形式:
    • t = {[k1] = v1, [k2] = v2}
    • t[k3] = v3
    • 3. 列表形式:
    • t = {1,2,’a’}
    • t[1] = 0
    • 从1开始的
    • 4.用分号也可以,通常用于分割不同类型的表元素
    • t = {k1 = v1; 1, 2}
    • 5. 遍历
    • for i = 1, #t do
    • ...
    • end
    • for k, v in pairs(t) do
    • ...
    • end
  • 全局变量存在_G中,可以通过_G.xxx来访问全局变量xxx
  • string和数字的转换:
    • tonumber()
    • tostring()
  • 条件语句的技巧:and和or返回的不是true和false
    • 1. 三元运算符: ( a and b) or c
    • 2. a and b==>a为false返回a,否则返回b
    • 3. a or b ==> a为true返回a,否则返回b
    • 4. a为nil或者false时赋值为b==> a = a or b
  • loadstring: 类似python的eval,可以求一个表达式,loadstring总是在全局环境中编译他的串
    • loadstring通常用于运行程序外部的代码,比如运行用户自定义的代码。注意:loadstring期望一个chunk,即语句。如果想要加载表达式,需要在表达式前加return,那样将返回表达式的值。
    • 例1:
    • f = loadstring("i = i + 1")
    • i = 0
    • f(); print(i) --> 1
    • 例2:
    • local l = io.read()
    • local func = assert(loadstring("return " .. l))
    • print("the value of your expression is " .. func())
  • 链表
    • list = nil
    • list = {next = list, value = "1"}
  • loadstring loadfile dofile
    • dofile, Lua 运行代码段的一种原始的操作。dofile 实际上是一个辅助的函数。真正完成功能的函数是 loadfile
    • loadfile 编译代码成中间码并且返回编译后的 chunk 作为一个函数,而不执行代码。loadfile只需要编译一次,可以多次运行;dofile却要每次都编译(适用于完成比较简单的功能)。 Lua 把每一个 chunk 都作为一个匿名函数处理。需要f=loadfile(...), f() == dofile(...)
    • loadstring 与 loadfile 相似,只不过它不是从文件里读入 chunk,而是从一个串中读入。loadstring 总 是在全局环境中编译他的串


控制语句

  • while:
    • while con do
    • ...
    • end
  • if … else:
    • if con then
    • ...
    • elseif con then
    • ...
    • else
    • ...
    • end
  • repeat:
    • repeat
    • ...
    • until con
  • break, return 只能出现在函数块的最后一句
  • for:
    • for i=start, end, [step] do
    • ...
    • end
    • for name in list do
    • ...
    • end
  • 泛型的for
    • 例如在table遍历中,pairs和ipairs
    • pairs:遍历所有的存在的项
    • ipairs: 从下标为1的开始遍历,直到没有下标对应的项为止
    • 泛型for的执行过程:
    • 首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
    • 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
    • 第三,将迭代函数返回的值赋给变量列表。
    • 第四,如果返回的第一个值为nil循环结束,否则执行循环体。
    • 第五,回到第二步再次调用迭代函数。
    • 如果我们的迭代函数是f,状态常量是s,控制变量的初始值是a0,那么控制变量将循环:a1=f(s,a0)、a2=f(s,a1)、……,直到ai=nil
  • 使用闭包的迭代器:保留上一次成功调用的状态和下一次成功调用的状态,用闭包实现
    • function list_iter(t)
    • local i = 0
    • return function()—迭代函数、状态常量为nil
    • i = i + 1
    • if i < table.getn(t) then return t[i] end—t[i]是控制变量
    • end
    • end
    • 使用:iter = list_iter(t)
    • while true do
    • ele = iter()
    • if else == nil then break end
    • end
    • 或者:
    • for ele in list_iter(t) do … end
    • 范性for在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量
    • 控制变量为nil时循环结束
    • for 控制变量 in 迭代函数 do
    • ...
    • end
  • 不使用闭包的迭代器
    • 无参数的迭代器----没有保留状态信息
      • ipairs就是这样的迭代器
      • function iter (a, i)—迭代函数iter,状态常量a,控制变量i
      • i = i + 1
      • local v = a[i]
      • if v then
      • return i, v
      • end
      • end
      • function ipairs (a)
      • return iter, a, 0
      • end
      • 依次调用iter(a, 1)返回1, a[1]; iter(a, 2)返回2, a[2]...
      • pairs使用了传统的next函数,pairs函数返回next, t, nil
    • 带参数的迭代器——有多个状态信息,而不是简单的状态常量+控制变量。除了闭包外,还可以用table实现:
      • local iter
      • function allwords()
      • local state = {line = io.read(), pos = 1}
      • return iter, state
      • end
      • function iterator (state)--状态常量
      • while state.line do -- repeat while there are lines
      • -- search for next word
      • local s, e = string.find(state.line, "%w+", state.pos)
      • if s then -- found a word?
      • -- update next position (after this word)
      • state.pos = e + 1
      • return string.sub(state.line, s, e)
      • else -- word not found
      • state.line = io.read() -- try next line...
      • state.pos = 1 -- ... from first position
      • end
      • end
      • return nil -- no more lines: end loop
      • end
    • 我们应该尽可能的写无状态的迭代器,因为这样循环的时候由for来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包;尽可能不要使用table这种方式,因为创建闭包的代价要比创建table小,另外Lua处理闭包要比处理table速度快些。
    • 另一种迭代器:
      • function allwords(f)
      • 遍历所有输入,并且对所有词w调用f(w)
      • end
      • 调用时:allwords(print)或者allwords(function(w) … end)


函数

function 函数名(参数列表)

… return 1

end

  • 返回值:返回值用,隔开,可以返回多个值;可以用{fun()}初始化得到返回值列表;可以(fun())只去第一个值
  • 参数:少了设为nil,多了丢弃
    • function f()
    • return 1, 2, 3, 4
    • end
    • x, y, z = f() =>x=1, y=2, z=3
  • 可变参数: 用三个点表示...
    • 参数存在名为arg的表中arg={1, 2; n=2}。可用ipairs(arg)访问,这样不会访问到n
  • 命名参数:Lua不支持,但是可以通过传一个字典的方式作为参数
    • function f(option)
    • option.key1...
    • end
    • f({key1 = 123})  或者 f{key1 = 123} 调用。因为只传一个参数,所以可以不用括号
  • 将函数作为参数
    • function g(f)
    • f(x)...
    • end
  • 闭包:在inner中可以访问outer中的变量,称为upvalue或者外部的局部变量。类似于全局遍量。。。会在两次调用f的过中保存(感觉有点像类)
    • function outer(a)
    • i = 0
    • return inner(b)
    • i+=1
    • 内容以及return
    • end
    • end
    • f = outer(a)
    • f(b)即具体的内容
    • 两次调用f(b)的时候,i分别为1和2
    • 如果再次调用g = outer(a),重新产生upvalue i = 0
    • 调用g的时候对应的i=0会变化;但是调用f的时候使用的是f对应的i
  • 闭包还可以用于重定义函数:
    • oldSin = math.sin
    • math.sin = function (x)
    • return oldSin(x * math.pi / 180)
    • end
  • 闭包还可以用来创建安全的运行环境
    • local oldOpen = io.open
    • io.open = function (file, mode)
    • if cannon(file, mode)
    • return oldOpen(file, mode)
    • else
    • return nil
    • ends
    • end
  • 无名函数:
    • f = function(x) return x + 1 end
    • 相当于
    • function f(x) return x + 1 end
  • 局部函数
    • local f = function ….
    • 或者 local function f….
    • 问题:注意递归调用自身时会出问题,因为Lua编译时会在全局找该函数,所以需要提前声明 local f
    • local f
    • f = function...
  • 尾调用:当函数最后一个动作是调用另一个函数时,称为尾调用。这种情况下函数不需要返回,可以直接到调用的函数中去,因此不会用额外的栈
    • 以下不是尾调用
    • return g() + 1
    • return (g())
    • return x or g()
  • metaTable和metaFunction:可以实现重载操作符
    • 当Lua试图对两个表进行相加时,他会检查两个表是否有一个表有Metatable,并且检查Metatable是否有__add域。如果找到则调用这个__add函数(所谓的Metamethod)去计算结果。
    • Lua中的每一个表都有其Metatable。(后面我们将看到userdata也有Metatable),Lua默认创建一个不带metatable的新表
    • 通过setmetatable设定
    • 对于每一个算术运算符,metatable都有对应的域名与其对应,除了__add、__mul外,还有__sub(减)、__div(除)、__unm(负)、__pow(幂),我们也可以定义__concat定义连接行为,关系运算符 __eq(等于),__lt(小于),和__le(小于等于),库定义的函数__tostring。
    • Lua选择metamethod的原则:如果第一个参数存在带有__add域的metatable,Lua使用它作为metamethod,和第二个参数无关;否则第二个参数存在带有__add域的metatable,Lua使用它作为metamethod 否则报错。
    • 关系运算的metamethods不支持混合类型运算。对于混合类型比较运算的处理方法和Lua的公共行为类似。如果你试图比较一个字符串和一个数字,Lua将抛出错误。相似的,如果你试图比较两个带有不同metamethods的对象,Lua也将抛出错误。
    • 例1:
      • a = {i=1}
      • b = {i=2}
      • addFun = {} --metaTable
      • function addFun.__add(p1, p2)—metaFunction
      • res = {}
      • res.i = p1.i + p2.i
      • return res
      • end
      • setmetatable(a, addFun)
      • setmetatable(b, addFun)
      • a + b可以用啦
    • 例2:
      • Set = {}
      • Set.mt = {} --metaTable
      • function Set.new()
      • local set = {}
      • setmetatable(set, Set.mt)
      • return set
      • end
      • Set.mt.__add = Set.union—metaFunction
    • print都会自动调用tostring
    • 如果你对metatable设置了__metatable的值,getmetatable将返回这个域的值,而调用setmetatable 将会出错:
      • Set.mt.__metatable = "123",此时调用getmetatable(Set.new())得到的是“123”,同时,也不能setmetatable
    • 表函数:针对在两种正常状态:表的不存在的域的查询和修改,Lua也提供了改变tables的行为的方法
      • 当我们访问一个表的不存在的域,返回结果为nil,这是正确的,但并不一定正确。实际上,这种访问触发lua解释器去查找__index metamethod:如果不存在,返回结果为nil;如果存在则由__index metamethod返回结果。
      • __index metamethod在继承中的使用非常常见,所以Lua提供了一个更简洁的使用方式。__index metamethod不需要非是一个函数,他也可以是一个表。当它是一个函数的时候,Lua将table和缺少的域作为参数调用这个函数;当他是一个表的时候,Lua将在这个表中看是否有缺少的域。
      • 应用1:默认值
      • Window = {}
      • Window.prototype = {x=0, y=0, width=100, height=100, }
      • Window.mt = {}
      • function Window.new (o)
      • setmetatable(o, Window.mt)
      • return o
      • end
      • Window.mt.__index = function (table, key)
      • return Window.prototype[key] --可以实现当没有指定的时候返回默认值
      • end
      • w = Window.new{x=10, y=20}
      • 应用2:默认值d的表t
      • local mt = {__index = function () return d end} --为每一个需要默认值的表创建了一个新的metatable。在有很多的表需要默认值的情况下,这可能使得花费的代价变大。也就是说,k个表需要k个metatable
      • 修改:local mt = {__index = function (t) return t[key] end} --使用表t的一项作为默认值,这里的key={}是一个表,仅仅作为键用。此时k个表只要1个metatable
      • 应用3:监控一个表的访问
      • __index和__newindex仅在没有对应项的时候会访问,所以要设一个空表作为proxy
      • 输入:t
      • _t = t, t = {} --t是一个proxy
      • 给t设置metatable,__index和__newindex均是函数,处理_t
      • 输出:t
      • 监视多个表:我们只要将每一个proxy和他原始的表关联,所有的proxy共享一个公用的metatable即可。将表和对应的proxy关联的一个简单的方法是将原始的表作为proxy的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的key。
      • 输入:t
      • 新建proxy = {key = t}, key={}
      • 给proxy设置metatable, __index和__newindex的对应函数处理proxy[key]。此时k个proxy均只用一个metatable
      • 输出:proxy
      • 应用4:只读表
      • 输入:t
      • 新建proxy, 它的metatable中的__index = t, __newindex = 调用error的函数
      • 输出:proxy
      • __index用来访问,__newindex用来更新
      • 有一个raw函数可以绕过metamethod:调用rawset(t,k,v)不调用任何metamethod将表t的k域赋值为v


模块

  • 载入.lua模块且立即执行:require(“模块名”)。多个对相同文件的require只会载入第一个,require会搜索目录加载文件
    • 调用require "lili"时会试着打开这些文件
    • lili
    • lili.lua
    • c:\windows\lili
    • /usr/local/lua/lili/lili.lua
  • local module = loadfile(“模块名”)。每次都会重新载入
    • module()调用模块
  • module:
    • module 指令运行完后,整个环境被压栈,所以前面全局的东西再看不见了。比如定义了一个 test 模块,使用module("test")后,下面不再看的见前面的全局环境。
    • 如果在这个模块里想调用 print 输出调试信息怎么办呢?一个简单的方法是
    • local print=print
    • module("test")
    • 这样 print 是一个 local 变量,下面也是可见的。或者可以用
    • local _G=_G
    • module("test")
    • 那么 _G.print 也是可以用的。
    • t = require("other")
    • t.xxx() 访问other包中的函数
    • 和require的不同:require只会寻找package.loaded[包名],module不仅和package一样,还会在全局表中存在name指定的表时,此表作为module。
  • 创建模块方法:
    • 1. 使用全局的表
    • 创建 complex = {}

      function complex.new() ...end

      complex.i = complex.new(0, 1)

      ...

      return complex

      注意点 首先,我们对每一个函数定义都必须显示的在前面加上包的名称。

      第二,同一包内的函数相互调用必须在被调用函数前指定包名。

      第三,这个return语句并非必需的,因为package已经赋值给全局变量complex了。但是,我们认为package打开的时候返回本身是一个很好的习惯。

      另一种创建方法 可以增加私有函数 local function a()

      注意,没有complex.a

      同时,可以修改为使用局部的表

      local P = {}

      complex = P

      functionP.new() ...end

      P.i =P.new(0, 1)

      ...

      return P

    • 2.全部使用局部函数
    • 创建
      local function new() ...end

      local i =new(0, 1)

      ...

      complex = { new = new, i = i ... } --函数映射,local的new变成complex.new

      return complex

    • 3.package使用独占的环境,不仅所有它的函数共享环境,而且它的所有全局变量也共享这个环境。我们可以将所有的公有函数声明为全局变量
    • 3.1 local P = {}

      complex = P

      setfenv(1, P) --将全局环境表注册为package的名字

      function add (c1, c2) --add 自动变成complex.add

      return new(c1.r + c2.r, c1.i + c2.i)

      end

      3.2 以上的问题:P作为我们的环境,我们就失去了访问所有以前的全局变量。下面有好几种方法可以解决这个问题,但都各有利弊。

      使用继承

      local P = {}

      setmetatable(P, {__index = _G})

      setfenv(1, P)

      3.3 声明一个局部变量保存老的环境

      local P = {}

      pack = P

      local _G = _G

      setfenv(1, P)

      现在,你必须对外部的访问加上前缀_G.,但是访问速度更快,因为这不涉及到metamethod。

      3.4 只把你需要的函数或者packages声明为local

      local P = {}

      pack = P

      local sqrt = math.sqrt

      local io = io

      setfenv(1, P)

  • 自动加载文件:使用到这个函数的时候才去加载
    • 先创建一个location表,对应了函数名和所在路径
    • 其次创建一个package表,并设置__index的metamethod,去loadfile(location[functionName]),加载了文件,同时返回函数作为访问的结果。
    • 最后返回package[functionName]
  • 包名和文件名统一:通过_REQUIREDNAME 得到文件名,当require加载一个文件的时候,它定义了一个变量来表示虚拟的文件名
    • local P = {}      -- package
    • if _REQUIREDNAME == nil then --如果不是通过require加载的,则_REQUIREDNAME 为nil
    • complex = P
    • else
    • _G[_REQUIREDNAME] = P
    • end
  • 热更新的实现:package.loaded[packageName]设为nil
  • math库:
    • 在弧度单位下工作
    • math.random([n [, m]]) 有三种用法: 无参调用, 产生 (0,1) 之间的浮点随机数; 只有参数 n, 产生 1-n 之间的整数; 有两个参数 n, m, 产生 n-m 之间的随机整数
  • table库:处理table,可以插入、删除、排序等
    • table.getn(t)
    • table.setn(t, size)
    • table.insert(t, sth) —在最后位置插入元素
    • table.insert(t, pos, sth) —在某一位置插入元素,位置为1开始
    • table.remove(t) --最后位置删除元素
    • table.remove(t, pos) —在某一位置删除元素,后面的会移动
    • 使用ipairs而不是pairs遍历数组的原因。前者使用key的顺序1、2、……,后者表的自然存储顺序
    • table.sort(t, f) f为排序函数,小于返回true
    • 如果要对一个有键值的数组排序,需要为其键创建一个数组,排序该数组,之后t[a[i]]的方式输出值。
    • 下面是一个用闭包迭代器生成值的例子:
    • function pairsByKeys (t, f)
    • local a = {}
    • for n in pairs(t) do table.insert(a, n) end
    • table.sort(a, f)
    • local i = 0 -- iterator variable
    • local iter = function () -- iterator function
    • i = i + 1
    • if a[i] == nil then return nil
    • else return a[i], t[a[i]]
    • end
    • end
    • return iter
    • end
    • for name, line in pairsByKeys(lines) do
    • print(name, line)
    • end
  • string库:
    • string.len(s)
    • string.rep(s, n) 重复s n次
    • string.sub(s, start, end) 为负数是指倒数第i个,-1是最后一个,不改变s,包含end
    • string.upper/lower(s)
    • string.format
    • string.char(1, 2, 3) 将数字转变成string
    • string.byte(s, i) 将字符串的第i位转换成数字
  • 模式匹配函数
    • string.find(s, 匹配, [起始位置]) 查找,返回开始索引和结束索引/ nil
    • string.gfind 适合用于范性for循环。他可以遍历一个字符串内所有匹配模式的子串。调用gfind函数的时候,如果不显示的指定捕获,函数将捕获整个匹配模式。
    • string.gsub 全局字符串替换
    • string.gmatch
    • while true do
    • i = string.find(s, "\n", i+1) -- find ‘next‘ newline
    • if i == nil then break end
    • table.insert(t, i)
    • end
  • 模式:
    • . 任意字符
    • %a 字母
    • %c 控制字符
    • %d 数字
    • %l 小写字母
    • %p 标点字符
    • %s 空白符
    • %u 大写字母
    • %w 字母和数字
    • %x 十六进制数字
    • %z 代表0的字符
    • 上面字符类的大写形式表示小写所代表的集合的补集
    • []方括号将字符类或者字符括起来创建自己的字符类
    • ‘[^0-7]‘ 匹配任何不是八进制数字的字符, ^代表补集,-代表范围
    • + 匹配前一字符1次或多次 最长匹配
    • * 匹配前一字符0次或多次 最长匹配
    • - 匹配前一字符0次或多次 最短匹配,一般和.配
    • ? 匹配前一字符0次或1次
    • %b匹配特定的字符,比如%b(), %b[], %b%{%} 以..开头,以…结尾的字符
    • ^首
    • $尾
    • () 可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个capture。
    • 例1:获取捕获内容
    • pair = "name = Anna"
    • _, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)”) —第一和第二个返回值是find的结果的开始和结束下标,用_这个哑元接收
    • print(key, value) --> name Anna
    • 例2:替换捕获内容
    • print(string.gsub("hello Lua", "(.)(.)", "%2%1”)) --第一个捕获和第二个捕获
    • --> ehll ouLa
    • 例3:用值替换字符串
    • 我们可以使用一个函数作为string.gsub的第三个参数调用gsub。在这种情况下,string.gsub每次发现一个匹配的时候就会调用给定的作为参数的函数,捕获值可以作为被调用的这个函数的参数,而这个函数的返回值作为gsub的替换串
    • function expand (s)
    • s = string.gsub(s, "$(%w+)", function (n)
    • return _G[n]
    • end)
    • return s
    • end
    • name = "Lua"; status = "great"
    • print(expand("$name is $status, isn‘t it?"))
    • --> Lua is great, isn‘t it?
    • 例4:计算字符串的值
    • s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]"
    • print((string.gsub(s, "$(%b[])", function (x)
    • x = "return " .. string.sub(x, 2, -2)
    • local f = loadstring(x)
    • return f()
    • end)))
    • --> sin(3) = 0.1411200080598672; 2^5 = 32
    • 例5:替换字符串
    • words = {}
    • s = "hello hi, again!"
    • for w in string.gfind(s, "(%a)") do
    • table.insert(words, w)
    • end
    • 例6:unescape,escape
    • function escape (s)
    • s = string.gsub(s, "([&=+%c])", function (c)
    • return c
    • end)
    • s = string.gsub(s, " ", "+")
    • return s
    • end
    • function unescape (s)
    • s = string.gsub(s, "+", " ")
    • s = string.gsub(s, "%%(%x%x)", function (h)
    • return string.char(tonumber(h, 16))
    • end)
    • return s
    • end
    • 例7:键值对解码
    • cgi = {}
    • function decode (s)
    • for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do
    • name = unescape(name)
    • value = unescape(value)
    • cgi[name] = value
    • end
    • end
  • io库:分为简单io和完全io模式
    • 简单模式的所有操作都是在两个当前文件之上。I/O库将当前输入文件作为标准输入(stdin),将当前输出文件作为标准输出(stdout)。这样当我们执行io.read,就是在标准输入中读取一行。我们可以使用io.input和io.output函数来改变当前文件。例如io.input(filename)就是打开给定文件(以读模式),并将其设置为当前输入文件。
      • string.format
      • io.write():在编写代码时应当避免像io.write(a..b..c);这样的书写,这同io.write(a,b,c)的效果是一样的。但是后者因为避免了串联操作,而消耗较少的资源。
      • 原则上当你进行粗略(quick and dirty)编程,或者进行排错时常使用print函数。当需要完全控制输出时使用write。Write函数与print函数不同在于,write不附加任何额外的字符到输出中去,例如制表符,换行符等等。还有write函数是使用当前输出文件,而print始终使用标准输出。另外print函数会自动调用参数的tostring方法,所以可以显示出表(tables)函数(functions)和nil。
      • io.read(): read函数从当前输入文件读取串,由它的参数控制读取的内容:
      • "*all" 读取整个文件
      • "*line" 读取下一行
      • "*number" 从串中转换出一个数值
      • num 读取num个字符到串
      • 在任何情况下,都应该考虑选择使用io.read函数的 " *.all " 选项读取整个文件,然后使用gfind函数来分解
      • 特别的,io.read(0)函数的可以用来测试是否到达了文件末尾。如果不是返回一个空串,如果已是文件末尾返回nil。
  • 完全模式的核心在于文件句柄(file handle)。该结构类似于C语言中的文件流(FILE*),其呈现了一个打开的文件以及当前存取位置。
    • 打开一个文件的函数是io.open。它模仿C语言中的fopen函数,同样需要打开文件的文件名参数,打开模式的字符串参数。模式字符串可以是 "r"(读模式),"w"(写模式,对数据进行覆盖),或者是 "a"(附加模式)。并且字符
      "b" 可附加在后面表示以二进制形式打开文件。正常情况下open函数返回一个文件的句柄。如果发生错误,则返回nil,以及一个错误信息和错误代码。
    • 文件打开后就可以用read和write方法对他们进行读写操作。它们和io表的read/write函数类似,但是调用方法上不同,必须使用冒号字符,作为文件句柄的方法来调用。
    • local f = assert(io.open(filename, "r"))
    • local t = f:read("*all")
    • f:close()
    • 由于通常Lua中读取整个文件要比一行一行的读取一个文件快的多。尽管我们有时候针对较大的文件(几十,几百兆),不可能把一次把它们读取出来。要处理这样的文件我们仍然可以一段一段(例如8kb一段)的读取它们。
    • 函数tmpfile函数用来返回零时文件的句柄,并且其打开模式为read/write模式。该临时文件在程序执行完后会自动进行清除。
    • 函数flush用来应用针对文件的所有修改。
    • 函数seek用来得到和设置一个文件的当前存取位置。
    • 在Unix中二进制文件和文本文件并没有区别,但是在如Windows这样的系统中,二进制文件必须以显式的标记来打开文件。控制这样的二进制文件,你必须将“b”标记添加在io.open函数的格式字符串参数中。
  • debug库:debug库由两种函数组成:自省(introspective)函数和hooks。自省函数使得我们可以检查运行程序的某些方面,比如活动函数栈、当前执行代码的行号、本地变量的名和值。Hooks可以跟踪程序的执行情况。Debug库中的一个重要的思想是栈级别(stack
    level)。一个栈级别就是一个指向在当前时刻正在活动的特殊函数的数字,也就是说,这个函数正在被调用但还没有返回。调用debug库的函数级别为1,调用他(他指调用debug库的函数)的函数级别为2,以此类推。
    • 自省:
      • local info = debug.getinfo(level, "Sl") --第一个参数可以是一个函数或者栈级别。getinfo可选的第二个参数可以用来指定选取哪些信息。指定了这个参数之后,程序不会浪费时间去收集那些用户不关心的信息。这个参数的格式是一个字符串,每一个字母代表一种类型的信息
      • info.what。。。
      • debug.getlocal 函数可以访问任何活动状态的局部变量。这个函数由两个参数:将要查询的函数的栈级别和变量的索引。
      • debug.setupvalue/debug.getupvalue getupvalue的第一个参数不是栈级别而是一个函数(精确的说应该是一个闭包),第二个参数是upvalue的索引
    • Hook:hook是这样一种机制:注册一个函数,用来在程序运行中某一事件到达时被调用。
    • 有四种可以触发一个hook的事件:
      • 当Lua调用一个函数的时候call事件发生;
      • 每次函数返回的时候,return事件发生;
      • Lua开始执行代码的新行时候,line事件发生;
      • 运行指定数目的指令之后,count事件发生。
      • debug.sethook 函数来注册一个hook:第一个参数是hook函数;第二个参数是一个描述我们打算监控的事件的字符串;可选的第三个参数是一个数字,描述我们打算获取count事件的频率。为了监控call、return和line事件,可以将他们的第一个字母(‘c‘、‘r‘ 或 ‘l‘)组合成一个mask字符串即可。要想关掉hooks,只需要不带参数地调用sethook即可。

面向对象/prototype

  • 函数访问:在一个函数内部使用全局变量名Account是一个不好的习惯。首先,这个函数只能在这个特殊的对象(译者:指Account)中使用;第二,即使对这个特殊的对象而言,这个函数也只有在对象被存储在特殊的变量(译者:指Account)中才可以使用。如果我们改变了这个对象的名字,函数将不能工作
    • 例:function Account.f()
    • Account.count = Account.count + 1
    • end
    • a = Account
    • Account = nil
    • a.f()会报错
    • 一个灵活的方法是:定义方法的时候带上一个额外的参数,来表示方法作用的对象。这个参数经常为self或者this。Lua提供了通过使用冒号操作符来隐藏这个参数的声明。我们可以使用dot语法定义函数而用冒号语法调用函数,反之亦然
  • :实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原型对象为类,其它对象为类的instance)
    • 设有对象a和b,将b作为a的prototype: setmetatable(a, {__index = b}), a中可以访问b的属性了。对象a调用任何不存在的成员都会到对象b中查找。术语上,可以将b看作类,a看作对象。
    • 对类创建方法:
      • A = {在这里设置初始值, val = 0}
      • 为类创建new方法和toString方法
      • function A:new(p)—self就是A
      • local obj = p
      • if obj == nil then
      • —create new obj
      • end
      • self.__index = self                —怕self被扩展以后改写
      • return setmetatable(obj, self)—该对象的metatable是A,返回第一个参数的值
      • end
      • function A:fun()
      • self.val = self.val + 1
      • end
      • A:new(p)相当于A.new(self, p)
      • 创建对象:A:new() — 创建默认的A对象
      • A:new{x = 1, y = 2}
      • 打印对象:对象:fun()。
      • 会发生的事:getmetatable(对象).__index.fun(对象, 100.00)。A.__index 还是A,所以会调用A的fun
      • 其中self.val没有定义,所以同上方法,取得了A.val = 0;第一个self.val是对象的val的声明
      • 所以相当于 对象.val = A.val + 1
  • 继承
    • B = A:new()
    • b = B:new{val=1}
    • B是A的一个实例,B从A继承了new方法,当new执行的时候,self参数指向B。所以,b的metatable是B,__index 也是B。这样,b继承了B,后者继承了A。
    • b:fun()在b中找不到fun,于是在B中找。B中也找不到,于是到A中找
  • 单例模式:使用闭包
    • function newObject (value)
    • return function (action, v) --返回的是一个函数
    • if action == "get" then return value
    • elseif action == "set" then value = v
    • else error("invalid action")
    • end
    • end
    • end
    • d = newObject(0)
    • print(d("get"))      --> 0
    • d("set", 10)
    • print(d("get"))      --> 10
  • 多重继承:实现的关键在于:将函数用作__index
  • 以下是createClass(args)函数的流程
  • 创建一个类c,它的父类为args[]
  • c的metatable中的__index是一个函数,这个函数会在args[]中寻找有对应下标k的,然后返回args[][k]的值(对应了某个函数或者域)
  • 将c的__index设为c
  • 另一方面,对c创建一个new方法
  • function c:new (o)
  • o = o or {}
  • setmetatable(o, c)
  • return o
  • end
  • 最后返回c
  • X = createClass(A, B)
  • obj = X:new()
  • obj:fun()
  • 发生的事情:Lua在obj中找不到fun,因此他查找obj的metatable的__index,即X。但是,X也没有fun,因此Lua查找X的metatable的__index,因为这个域包含一个函数,Lua调用这个函数并首先到A中查找fun,没有找到,然后到B中查找,找到并返回最终的结果。
  • 私有性:每个对象用两个表来表示:一个描述状态;另一个描述操作(或者叫接口)。对象本身通过第二个表来访问,也就是说,通过接口来访问对象。为了避免未授权的访问,表示状态的表中不涉及到操作;表示操作的表也不涉及到状态,取而代之的是,状态被保存在方法的闭包内。
  • 一种方法是:你不想访问一个对象内的一些东西就不要访问
  • 另一种方法如下:
  • function newAccount (initialBalance)
  • local self = {balance = initialBalance} 数据
  • local withdraw = function (v)
  • self.balance = self.balance - v
  • end
  • local deposit = function (v)
  • self.balance = self.balance + v
  • end
  • local getBalance = function () return self.balance end
  • return { 函数
  • withdraw = withdraw,
  • deposit = deposit,
  • getBalance = getBalance
  • }
  • end
  • 此时用.访问
  • acc1 = newAccount(100.00)
  • acc1.withdraw(40.00)
  • print(acc1.getBalance())

和C的交互

  • Lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数
  • 实例:检测是否调用成功,然后调用初始化函数
    • local f = assert(loadlib(path, "luaopen_socket"))
    • f()

异常处理

  • 抛出异常
    • assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出
    • 调用error函数显式地抛出错误,error的参数是要抛出的错误信息
    • file = assert(io.open("no-file", "r”))  io.open返回的第二个结果(错误信息)会作为assert的第二个参数。
  • 处理异常
    • Lua中需要处理错误,需要使用pcall函数封装你的代码:假设函数f可能抛出异常,此时使用
    • ifpcall(f) then —抛出异常的代码在函数中,函数没有参数 或者用匿名函数
      pcall(function() … end)
    • -- no errors while running `foo‘
    • ...
    • else
    • -- `foo‘ raised an error: take appropriate actions
    • ...
    • end
    • pcall在保护模式(protected mode)下执行函数内容,同时捕获所有的异常和错误。若一切正常,pcall返回true以及“被执行函数”的返回值;否则返回nil和错误信息。
    • 错误信息不一定仅为字符串(下面的例子是一个table),传递给error的任何信息都会被pcall返回:
    • local val, err = pcall(function () error({code=121}) end)
    • pcall返回错误信息时,已经释放了保存错误发生情况的栈信息。因此,若想得到tracebacks,我们必须在pcall返回以前获取。
    • Lua提供了xpcall来实现这个功能,xpcall接受两个参数:调用函数、错误处理函数。当错误发生时,Lua会在栈释放以前调用错误处理函数,因此可以使用debug库收集错误相关信息。
    • 有两个常用的debug处理函数:debug.debug和debug.traceback,前者给出Lua的提示符,你可以自己动手察看错误发生时的情况;后者通过traceback创建更多的错误信息,也是控制台解释器用来构建错误信息的函数。你可以在任何时候调用debug.traceback获取当前运行的traceback信息
    • status, err = xpcall(function() foo(1) end, function() print(debug.traceback()) end)
    • —foo是带参数的会抛出异常的函数,需要外面套一层
    • --第二个参数打印出来了异常

协同处理

  • Lua的所有协同函数存放于coroutine table中。create函数用于创建新的协同程序,其只有一个参数:一个函数,即协同程序将要运行的代码。若一切顺利,返回值为thread类型,表示创建成功。
    • t = coroutine.create(function ()  print("hi”)  end)
  • 协同有三个状态:挂起态(suspended)、运行态(running)、停止态(dead)。当我们创建协同程序成功时,其为挂起态,即此时协同程序并未运行。我们可用status函数检查协同的状态:
    • coroutine.status(t)
    • coroutine.resume(t)运行
    • coroutine.yield() 挂起,一般写在函数里
    • 相互传递参数:
    • co = coroutine.create(function (a,b) coroutine.yield(a + b, a - b) end)
    • 1. 主程序中通过 coroutine.resume(co, 1, 2) 将a和b传给co
    • 2. co通过yield将结果返回给resume,resume返回值为true, 3, -1
    • print(coroutine.resume(co, 1, 2)) —true 3 -1
    • 3. 如果yield改为coroutine.yield(),则会返回resume中传入的参数1,2
    • 4. 如果create中的函数有返回值,也会传递给coroutine.resume
    • 和python中的generator类似,由于yield和resume是由不同的函数调用的,所以也成为半协同
  • 协同用作迭代器
    • function perm (list)
    • local n = table.getn(list)
    • local co = coroutine.create(function () permgen(list, n) end)
    • return function () — 闭包iterator
    • local code, res = coroutine.resume(co)
    • return res
    • end
    • end
    • permgen(list, n)中生成结果的地方改成yield
  • 非抢占式的,当一个协同正在运行时,不能在外部终止他。只能通过显示的调用yield挂起他的执行。对非抢占式多线程来说,不管什么时候只要有一个线程调用一个阻塞操作(blocking operation),整个程序在阻塞操作完成之前都将停止。

数据存储例子

  • Lua数据:
    • entry{key="val",...}
    • 有多个entry项,相当于entry({...}),可以看做是一个函数调用
    • 使用dofile("db.lua")完成调用
  • 使用:
    • 1. 定义entry函数
    • function entry1(e)
    • e.key1...
    • end
    • 2. 调用entry函数
    • entry = entry1
    • dofile("db.lua")

环境

  • Lua用一个名为environment普通的表来保存所有的全局变量。Lua将环境本身存储在一个全局变量_G中,(_G._G等于_G)
  • _G["x"]中存的是x的值,相当于loadstring("x")
  • 对于a.b.c.d的形式的变量,需要递归遍历表。设置值的时候需要(a.b.c).d=v
    • function getfield (f)
    • local v = _G      -- start with the table of globals
    • for w in string.gfind(f, "[%w_]+") do
    • v = v[w]
    • end
    • return v
    • end
    • function setfield (f, v)
    • local t = _G         -- start with the table of globals
    • for w, d in string.gfind(f, "([%w_]+)(.?)") do
    • if d == "." then  -- not last field?
    • t[w] = t[w] or {}    -- create table if absent
    • t = t[w]          -- get the table
    • else                 -- last field
    • t[w] = v          -- do the assignment
    • end
    • end
    • end
  • 应用1:控制全局变量,不允许出现没有申明的变量
    • 将_G的metatable中的__index和__newindex均抛出异常
    • 通过rawset和rawget申明和检查某个变量是否存在
    • function declare (name, initval)
    • rawset(_G, name, initval or false)
    • end
    • 注意:需要将值为nil的变量值设为false,否则直接访问的时候仍然会调用_G的metatable中的__index
    • 或者维护一个声明了的变量名的表,__index和__newindex的时候检查变量名是否存在
  • 上面的问题是:如果你想使用标准库,标准库中可能使用到没有声明的全局变量,你将悲剧。Lua 5.0允许每个函数可以有自己的环境。setfenv函数来改变一个函数的环境。Setfenv接受函数和新的环境作为参数。除了使用函数本身,还可以指定一个数字表示栈顶的活动函数。数字1代表当前函数,数字2代表调用当前函数的函数(这对写一个辅助函数来改变他们调用者的环境是很方便的)依此类推。一旦你改变了你的环境,所有全局访问都使用这个新的表,如果她为空,你就丢失所有你的全局变量,甚至_G,所以,你应该首先使用一些有用的值存储_G
    • a = 1  -- create a global variable
    • -- change current environment
    • setfenv(1, {_G = _G})
    • _G.print(a)       --> nil
    • _G.print(_G.a)    --> 1
  • 或者:
    • setmetatable(newgt, {__index = _G})
    • setfenv(1, newgt)    -- set it
    • print(a)             --> 1
    • a = 10
    • print(a)      --> 10
    • print(_G.a)   --> 1
  • 当你创建一个新的函数时,他从创建他的函数继承了环境变量。所以,如果一个chunk改变了他自己的环境,这个chunk所有在改变之后定义的函数都共享相同的环境,都会受到影响。这对创建命名空间是非常有用的机制。

Weak表

  • Lua GC
    • Lua自动进行内存的管理。程序只能创建对象(表,函数等),而没有执行删除对象的函数。
    • Lua的垃圾收集器不存在循环的问题。在使用循环性的数据结构的时候,你无须加入特殊的操作;他们会像其他数据一样被收集。
    • 垃圾收集器只能在确认对象失效之后才会进行收集;它是不会知道你对垃圾的定义的。一个典型的例子就是堆栈:有一个数组和指向栈顶的索引构成。你知道这个数组中有效的只是在顶端的那一部分,但Lua不那么认为。如果你通过简单的出栈操作提取一个数组元素,那么数组对象的其他部分对Lua来说仍然是有效的。同样的,任何在全局变量中声明的对象,都不是Lua认为的垃圾,即使你的程序中根本没有用到他们。这两种情况下,你应当自己处理它(你的程序),为这种对象赋nil值,防止他们锁住其他的空闲对象。一个典型的例子发生在当你想在你的程序中对活动的对象(比如文件)进行收集的时候。那看起来是个简单的任务:你需要做的是在收集器中插入每一个新的对象。然而,一旦对象被插入了收集器,它就不会再被收集!即使没有其他的指针指向它,收集器也不会做什么的。
    • Weak表是一种用来告诉Lua一个引用不应该防止对象被回收的机制。一个weak引用是指一个不被Lua认为是垃圾的对象的引用。如果一个对象所有的引用指向都是weak,对象将被收集,而那些weak引用将会被删除。Lua通过weak tables来实现weak引用:一个weak tables是指所有引用都是weak的table。这意味着,如果一个对象只存在于weak tables中,Lua将会最终将它收集。
  • 字符串的特殊性
    • 关于字符串的一些细微差别:从上面的实现来看,尽管字符串是可以被收集的,他们仍然跟其他可收集对象有所区别。其他对象,比如tables和函数,他们都是显示的被创建。比如,不管什么时候当Lua遇到{}时,它建立了一个新的table。任何时候这个 function()。。。end建立了一个新的函数(实际上是一个闭包)。然而,Lua见到“a”..“b”的时候会创建一个新的字符串?如果系统中已经有一个字符串“ab”的话怎么办?Lua会重新建立一个新的?编译器可以在程序运行之前创建字符串么?这无关紧要:这些是实现的细节。因此,从程序员的角度来看,字符串是值而不是对象。
  • 一般的表:keys和values都属于强引用。
  • weak tables中,keys和vaules都可能是weak的。
  • 那意味着这里存在三种类型的weak tables:weak keys组成的tables;weak values组成的tables;以及纯weak tables类型,他们的keys和values都是weak的。与table本身的类型无关,当一个keys或者vaule被收集时,整个项(entry)都将从这个table中消失。
  • 表的weak性由他的metatable的__mode域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个table中的keys就是weak的;如果这个字符串包含小写字母‘v’,这个table中的vaules就是weak的,‘kv‘则是全部weak的。
  • 要注意,只有对象才可以从一个weak table中被收集。比如数字和布尔值类型的值&字符串,都是不会被收集的。例如,如果我们在table中插入了一个数值型的key(在前面那个例子中),它将永远不会被收集器从table中移除。
    • a = {}
    • b = {}
    • setmetatable(a, b)
    • b.__mode = \"k\"     -- now ‘a‘ has weak keys
  • 应用1:记忆表,存储运算的结果,加快速度。使用weak table可以避免无限占用内存
  • 应用2:默认值d的表t
    • 方法1:用一个表存所有的t-d对
    • local defaults = {} --存储所有表的默认值,而不是让一个表存自己的默认值
    • setmetatable(defaults, {__mode = \"k\"})
    • local mt = {__index = function (t) return defaults[t] end}
    • function setDefault (t, d)
    • defaults[t] = d --t是一个table,当table被回收时,该项也被清除
    • setmetatable(t, mt)
    • end
    • 方法2:用一个表存d-metatable对
    • local metas = {}
    • setmetatable(metas, {__mode = \"v\"})
    • function setDefault (t, d)
    • local mt = metas[d]
    • if mt == nil then
    • mt = {__index = function () return d end}
    • metas[d] = mt     -- d对应一个metatable
    • end
    • setmetatable(t, mt)
    • end
  • 应用3:对象-属性表。如数组和数组的大小
时间: 2024-10-15 08:05:38

Lua总结的相关文章

cocos2dx lua中异步加载网络图片,可用于显示微信头像

最近在做一个棋牌项目,脚本语言用的lua,登录需要使用微信登录,用户头像用微信账户的头像,微信接口返回的头像是一个url,那么遇到的一个问题就是如何在lua中异步加载这个头像,先在引擎源码里找了下可能会提供这个功能的地方,发现好像没有提供类似功能,那么只能自己动手写.所以我在ImageView这个类里面添加了一个成员方法,其实可以不写在ImageView里,而且我觉得非必需情况下还是不要修改引擎源码的好,因为如果源码改动比较多的话,将来引擎版本升级会比较麻烦.我写在ImageView里纯粹是想偷

Lua 第一天

今天开始学习Lua语言,感觉Lua非常便捷.我用的编译器是SciTE,很不错. 举例一:无需引用,内置输出语句   print() print(6)   --> 6 print(type(6))  -->   number 举例二:对数字字符串进行数字化处理 print("2"+3 )  -->5 举例三:..连接俩个字符串,#返回字符串长度 举例四:print()输出函数,可以有多个输出值 a = 1,b =2,c = a+b    print(a,b,c,a,b,

lua协程一则报错解决“attempt to yield across metamethod/C-call boundary”

问题 attempt to yield across metamethod/C-call boundary 需求跟如下帖子中描述一致: http://bbs.chinaunix.net/forum.php?mod=viewthread&action=printable&tid=4065715 模拟一个场景,在C中创建出coroutine来执行Lua脚本,并且提供C API给Lua使用,当某些操作可能会阻塞时(如网络I/O),C函数中执行yield将协程切换出去,然后未来的某个时刻,如果条件

lua闭合函数

function count( ... ) local i = 0 return function( ... ) i = i+ 1 return i end end local func = count(...) print(func()) print(func()) print(func()) 结果如下: 1 2 3 [Finished in 0.1s] lua 闭合函数:一个函数加上该函数所需访问的所有“非局部变量”. 如上所示:count()函数返回了另一个函数,而这个函数使用了count

Mac下Lua Sublime Text2 开发环境搭建

1.安装Lua编译器 下载Lua, http://www.lua.org/: 解压后,cd进入该文件夹src目录下 在当前文件夹执行make macosx   然后回车 cd 到上一目录,执行sudo make install 完成之后执行lua -v 可以看到:Lua 5.3.3  Copyright (C) 1994-2016 Lua.org, PUC-Rio 2.安装Sublime Text2 1.下载http://sublime-text-2.cn.uptodown.com后打开: {

Nginx+Lua 积累

Nginx+Lua 积累 1.解析16进制编码的中文参数 复制代码 local encodeStr = "%E6%B0%94" local decodeStr = ""; for i = 2, #encodeStr - 1, 3 do local num = encodeStr:sub(i, i + 1); num = tonumber(num, 16); decodeStr = decodeStr .. string.char(num); end ngx.say(

nginx与Lua执行顺序

Nginx顺序 Nginx 处理每一个用户请求时,都是按照若干个不同阶段(phase)依次处理的,而不是根据配置文件上的顺序. Nginx 处理请求的过程一共划分为 11 个阶段,按照执行顺序依次是 post-read.server-rewrite.find-config.rewrite.post-rewrite. preaccess.access.post-access.try-files.content.log. post-read: 读取请求内容阶段 Nginx读取并解析完请求头之后就立即

在 Lua 里 使用 Cocos Studio 导出的 .csb 文件

1. 加载 节点到场景 第一种方法 local scene = cc.CSLoader:createNode("scene.csb") self:addChild(scene) 第二种方法 local scene = cc.uiloader:load("MainScene.csb"):addTo(self) 2.强转精灵类型 local sprite = tolua.cast(object,"cc.Sprite") CocoStudio 做的里面

深入lua栈交互—cpp调用lua数据

lua是通过lua_state这个栈来和c 交互的 1.....lua栈 index 下往上增长 如: 1 2 3 4 5 6 2.....lua栈 index 是循环的 如下 index 上到下 是 3 2 1 0 -1 -2 -3 ,栈对应的值为     1     2     3     x     1     2     3      3......lua函数多个返回值如果上面是function返回了3个返回值,那么return a ,b,c  中 a=3 b=2 c=1 第一个返回值先

[nginx] 由Lua 粘合的Nginx生态环境-- agentzh tech-club.org

[nginx] 由Lua 粘合的Nginx生态环境-- agentzh tech-club.org 演讲听录 [复制链接] kindle LT管理团队 Rank: 9Rank: 9Rank: 9 未绑定新浪微博 签到222 注册时间1970-1-1最后登录2015-6-5在线时间168 小时阅读权限200积分19025帖子119主题35精华2UID9223 LT总司令 LT元老 LT教授 串个门加好友打招呼发消息 电梯直达跳转到指定楼层 1# 发表于 2013-1-12 12:43:47 |只看