• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Lua初探

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
 

官方网站:http://www.lua.org/
三方模块:https://luarocks.org/

Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。 一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。

安装

curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz z代表gzip的压缩包;x代表解压;v代表显示过程信息;f代表后面接的是文件
cd lua-5.3.0
make linux test
make install

注意
报 lua.c:67:31: fatal error: readline/readline.h: No such file or directory,说明缺少libreadline-dev依赖包
centos: yum install readline-devel
debian: apt-get install libreadline-dev.

安装完成之后,Lua 提供了交互式编程模式。我们可以在命令行中输入程序并立即查看效果。

$ lua -i
$ Lua 5.3.2 Copyright (C) 1994-2015 Lua.org, PUC-Rio

luarocks

luarocks可以安装和更新lua的第三方模块。
可以在 http://luarocks.org/releases/ 页面选择需要的软件包。
安装教程:https://github.com/keplerproject/luarocks/wiki/Installation-instructions-for-Unix

语法

注释

两个减号是单行注释:

  1. print("Hello World!")--这个是单行注释

多行注释

  1. --[[
  2. print("Hello World!")
  3. print("Hello World!")
  4. --]]

标示符

Lua 表示符用于定义一个变量,函数获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上0个或多个字母,下划线,数字(0到9)。
最好不要使用下划线加大写字母的标示符,因为Lua的保留字也是这样的。
Lua 不允许使用特殊字符如 @, $, 和 % 来定义标示符。 Lua 是一个区分大小写的编程语言。因此在 Lua 中 W3c 与 w3c 是两个不同的标示符。

关键字

and break do else elseif end false for function if in local nil not or repeat return then true until while

全局变量

在默认情况下,变量总是认为是全局的。
全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。
换句话说, 当且仅当一个变量不等于nil时,这个变量即存在。

数据类型

Lua是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua中有8个基本类型分别为:nil、boolean、number、string、userdata、function、thread和table。

数据类型 描述
nil 只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示,用..来拼接字符串
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字或者是字符串。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。

变量

变量在使用前,必须在代码中进行声明,即创建该变量。编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。
Lua 变量有三种类型:全局变量、局部变量、表中的域。
函数外的变量默认为全局变量,除非用 local 显示声明。函数内变量与函数的参数默认为局部变量。
局部变量的作用域为从声明位置开始到所在语句块结束(或者是直到下一个同名局部变量的声明)。
变量的默认值均为 nil。

赋值

Lua可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。注意:如果要对多个变量赋值必须依次对每个变量赋值。

a, b = 10, 2x <–> a=10; b=2x

遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:

x, y = y, x – swap ‘x’ for ‘y’
a[i], a[j] = a[j], a[i] – swap ‘a[i]’ for ‘a[i]’

函数调用返回给变量
a, b = f()
f()返回两个值,第一个赋给a,第二个赋给b。

索引

对 table 的索引使用方括号 []

t[i]
t.i – 当索引为字符串类型时的一种简化写法
gettable_event(t,i) – 采用索引访问本质上是一个类似这样的函数调用

循环

  1. while(condition)
  2. do
  3. statements
  4. end
  5. --varexp1变化到exp2,每次变化以exp3为步长递增var,并执行一次"执行体"exp3是可选的,如果不指定,默认为1
  6. forvar=exp1,exp2,exp3 do
  7. <执行体>
  8. end
  9. for i=10,1,-1do
  10. print(i)
  11. end
  12. --打印数组a的所有值,i是数组索引值,v是对应索引的数组元素值。ipairsLua提供的一个迭代器函数,用来迭代数组。
  13. for i,v in ipairs(a)do
  14. print(v)
  15. end
  16. repeat
  17. statements
  18. while( condition )

循环控制语句
break 退出当前循环或语句,并开始脚本执行紧接着的语句。

流程控制

控制结构的条件表达式结果可以是任何值,Lua认为false和nil为假,true 和非nil为真。
要注意的是Lua中 0 为 true

  1. if(布尔表达式)
  2. then
  3. --[在布尔表达式为true时执行的语句--]
  4. else
  5. --[布尔表达式为false时执行该语句块--]
  6. end

函数

  1. optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
  2. function_body
  3. return result_params_comma_separated
  4. end

optional_function_scope
该参数是可选的制定函数是全局函数还是局部函数,未设置该参数末尾为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。
result_params_comma_separated
函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。

可变参数

  1. function average(...)
  2. result =0
  3. local arg={...}
  4. for i,v in ipairs(arg)do
  5. result = result + v
  6. end
  7. print("总共传入 "..#arg .. " 个数")
  8. return result/#arg
  9. end

Lua函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(…) 表示函数有可变的参数。
Lua将函数的参数放在一个叫arg的表中,#arg 表示传入参数的个数。
例如,我们计算几个数的平均值:
print(“平均值为”,average(10,5,3,4,5,6))

运算符

算术运算符
所有语言通用+-*/^%
关系运算符
所有语言通用,特例:不等于为~=
逻辑运算符
设定 A 的值为 true,B 的值为 false:
(A and B) 为 false。
(A or B) 为 true。
not(A and B) 为 true。
其他运算符
.. 连接两个字符串

一元运算符,返回字符串或表的长度。 #”Hello” 返回 5

优先级
从高到低的顺序:
^
not - (unary)
* /
+ -
..
< > <= >= ~= ==
and
or
除了^和..外所有的二元运算符都是左连接的。

字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。
Lua 语言中字符串可以使用以下三种方式来表示:

  • 单引号间的一串字符。
  • 双引号间的一串字符。
  • [[和]]间的一串字符。
  1. string1 ="Lua"
  2. print("\"字符串 1 是\"",string1)
  3. string2 ='w3cschool.cc'
  4. print("字符串 2 是",string2)
  5. string3 =[["Lua 教程"]]
  6. print("字符串 3 是",string3)

迭代器

泛型 for 迭代器

泛型 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。

  1. for k, v in pairs(t)do
  2. print(k, v)
  3. end

范性for的执行过程:
首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
第三,将迭代函数返回的值赋给变量列表。
第四,如果返回的第一个值为nil循环结束,否则执行循环体。
第五,回到第二步再次调用迭代函数

无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

  1. function square(iteratorMaxCount,currentNumber)
  2. if currentNumber<iteratorMaxCount
  3. then
  4. currentNumber = currentNumber+1
  5. return currentNumber, currentNumber*currentNumber
  6. end
  7. end
  8. for i,n in square,3
  9. do
  10. print(i,n)
  11. end

多状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table内,将table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数。

  1. array ={"Lua","Tutorial"}
  2. function elementIterator (collection)
  3. local index =0
  4. local count =#collection
  5. --闭包函数
  6. returnfunction()
  7. index = index +1
  8. if index <= count
  9. then
  10. --返回迭代器的当前元素
  11. return collection[index]
  12. end
  13. end
  14. end
  15. for element in elementIterator(array)
  16. do
  17. print(element)
  18. end

table

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数字、字典等。
Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。
Lua table 是不固定大小的,你可以根据自己需要进行扩容。
Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用”format”来索引table string。

  1. --初始化表
  2. mytable ={}
  3. --指定值
  4. mytable[1]="Lua"
  5. --移除引用
  6. mytable =nil
  7. -- lua 垃圾回收会释放内存

当我们初始化 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil ,则 b 同样能访问 table 的元素。如果没有指定的变量指向a,Lua的垃圾回收机制会清理相对应的内存。

模块与包

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。

  1. --文件名为module.lua
  2. --定义一个名为module的模块
  3. module={}
  4. --定义一个常量
  5. module.constant ="这是一个常量"
  6. --定义一个函数
  7. functionmodule.func1()
  8. io.write("这是一个公有函数!\n")
  9. end
  10. --表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.
  11. localfunction func2()
  12. print("这是一个私有函数!")
  13. end
  14. functionmodule.func3()
  15. func2()
  16. end
  17. returnmodule

require 函数

Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。
require(“<模块名>”)或者require “<模块名>”

  1. -- test_module.php 文件
  2. --module模块为上文提到到module.lua
  3. require("module")
  4. print(module.constant)
  5. module.func3()
  6. -- test_module2.php 文件
  7. --module模块为上文提到到module.lua
  8. --别名变量 m
  9. local m =require("module")
  10. print(m.constant)
  11. m.func3()

加载机制

对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。
require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。
当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 “~/lua/” 路径加入 LUA_PATH 环境变量里:

#LUA_PATH
export LUA_PATH=”~/lua/?.lua;;”

文件路径以 “;” 号分隔,最后的 2 个 “;;” 表示新加的路径后面加上原来的默认路径。
接着,更新环境变量参数,使之立即生效。

source ~/.profile

这时假设 package.path 的值是:

/Users/dengjoe/lua/?.lua;./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

那么调用 require(“module”) 时就会尝试打开以下文件目录去搜索目标。

/Users/dengjoe/lua/module.lua;
./module.lua
/usr/local/share/lua/5.1/module.lua
/usr/local/share/lua/5.1/module/init.lua
/usr/local/lib/lua/5.1/module.lua
/usr/local/lib/lua/5.1/module/init.lua

如果找过目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。
搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。
搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。

C 包

Lua和C是很容易结合的,使用C为Lua写包。
与Lua中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制。
Lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:

local path = “/usr/local/lua/lib/libluasocket.so”
local f = loadlib(path, “luaopen_socket”)

loadlib函数加载指定的库并且连接到Lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为Lua的一个函数,这样我们就可以直接在Lua中调用他。
如果加载动态库或者查找初始化函数时出错,loadlib将返回nil和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:

local path = “/usr/local/lua/lib/libluasocket.so”
– 或者 path = “C:\windows\luasocket.dll”,这是 Window 平台下
local f = assert(loadlib(path, “luaopen_socket”))
f() – 真正打开库

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的stub文件,安装二进制库的时候可以随便放在某个目录,只需要修改stub文件对应二进制库的实际路径即可。
将stub文件所在的目录加入到LUA_PATH,这样设定后就可以使用require函数加载C库了。

元表(Metatable)

在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。
因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。
当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫”__add”的字段,若找到,则调用对应的值。”__add”等即时字段,其对应的值(往往是一个函数或是table)就是”元方法”。
有两个很重要的函数来处理元表:

  • setmetatable(table,metatable): 对指定table设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败 。
  • getmetatable(table): 返回对象的元表(metatable)。

元方法

  • __index 元方法查看表中元素是否存在,如果不存在,返回结果为nil;如果存在则由__index 返回结果。
  • __newindex 元方法用来对表更新,__index则用来对表访问 。
    当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。
    *__add 键包含在元表中,并进行相加操作。其他还包括__sub __mul __div __mod __unm __concat __eq __lt __le
  • __call 元方法在 Lua 调用一个值时调用。
  • __tostring 元方法用于修改表的输出行为。

协同程序(coroutine)

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

线程和协同程序区别

线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

基本语法

方法 描述
coroutine.create() 创建coroutine,返回coroutine, 参数是一个函数,当和resume配合使用的时候就唤醒函数调用
coroutine.resume() 重启coroutine,和create配合使用
coroutine.yield() 挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果
coroutine.status() 查看coroutine的状态,注:coroutine的状态有三种:dead,suspend,running
coroutine.wrap() 创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复
coroutine.running() 返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号

coroutine在底层实现就是一个线程。
当create一个coroutine的时候就是在新线程中注册了一个事件。
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。

  1. function foo (a)
  2. print("foo 函数输出", a)
  3. return coroutine.yield(2* a)--返回2*a 的值
  4. end
  5. co = coroutine.create(function(a , b)
  6. print("第一次协同程序执行输出", a, b)-- co-body 110
  7. local r = foo(a +1)
  8. print("第二次协同程序执行输出", r)
  9. local r, s = coroutine.yield(a + b, a - b)-- ab的值为第一次调用协同程序时传入
  10. print("第三次协同程序执行输出", r, s)
  11. return b,"结束协同程序"-- b的值为第二次调用协同程序时传入
  12. end)
  13. print("main", coroutine.resume(co,1,10))--true,4
  14. print("--分割线----")
  15. print("main", coroutine.resume(co,"r"))--true11-9
  16. print("---分割线---")
  17. print("main", coroutine.resume(co,"x","y"))--true10end
  18. print("---分割线---")
  19. print("main", coroutine.resume(co,"x","y"))-- cannot resume dead coroutine
  20. print("---分割线---")
  21. --结果
  22. 第一次协同程序执行输出110
  23. foo 函数输出2
  24. main true4
  25. --分割线----
  26. 第二次协同程序执行输出 r
  27. main true11-9
  28. ---分割线---
  29. 第三次协同程序执行输出 x y
  30. main true10结束协同程序
  31. ---分割线---
  32. main false cannot resume dead coroutine
  33. ---分割线---

resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。

生产者-消费者问题

  1. local newProductor
  2. function productor()
  3. local i =0
  4. whiletruedo
  5. i = i +1
  6. send(i)--将生产的物品发送给消费者
  7. end
  8. end
  9. function consumer()
  10. whiletruedo
  11. local i = receive()--从生产者那里得到物品
  12. print(i)
  13. end
  14. end
  15. function receive()
  16. local status, value = coroutine.resume(newProductor)
  17. return value
  18. end
  19. function send(x)
  20. coroutine.yield(x)-- x表示需要发送的值,值返回以后,就挂起该协同程序
  21. end
  22. --启动程序
  23. newProductor = coroutine.create(productor)
  24. consumer()

文件I/O

Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式。

  • 简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
  • 完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法
    简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。

    file = io.open (filename [, mode])

模式 描述
r 以只读方式打开文件,该文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
r+ 以可读写方式打开文件,该文件必须存在。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a+ 与a类似,但此文件可读可写
b 二进制模式,如果文件是二进制文件,可以加上b
+ 号表示对文件既可以读也可以写

简单模式

  1. --以只读方式打开文件
  2. file = io.open("test.lua","r")
  3. --设置默认输入文件为 test.lua
  4. io.input(file)
  5. --输出文件第一行
  6. print(io.read())
  7. --关闭打开的文件
  8. io.close(file)
  9. --以附加的方式打开只写文件
  10. file = io.open("test.lua","a")
  11. --设置默认输出文件为 test.lua
  12. io.output(file)
  13. --在文件最后一行添加Lua注释
  14. io.write("-- test.lua 文件末尾注释")
  15. --关闭打开的文件
  16. io.close(file)

完全模式

通常我们需要在同一时间处理多个文件。我们需要使用 file:function_name 来代替 io.function_name 方法。

  1. --以只读方式打开文件
  2. file = io.open("test.lua","r")
  3. --输出文件第一行
  4. print(file:read())
  5. --关闭打开的文件
  6. file:close()
  7. --以附加的方式打开只写文件
  8. file = io.open("test.lua","a")
  9. --在文件最后一行添加Lua注释
  10. file:write("--test")
  11. --关闭打开的文件
  12. file:close()<

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
windows下配置lua开发环境发布时间:2022-07-22
下一篇:
Step By Step(Lua目录)发布时间:2022-07-22
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap