zoukankan      html  css  js  c++  java
  • Lua基础 编译、运行、错误处理

    尽管Lua是一门解析型的语言,但是在运行前也会被编译成某个中间状态。一门解析型的语言需要编译,这听起来有点不合常理。但是,实际上,解析型语言的与众不同,不是说它不需要编译,而是说它把编译作为其运行时的一部分,因此,它就可以执行各种来自外部的代码(例如网上的)。也许因为Lua中存在的如dofile 这样的函数,才使Lua可以被称为一门解析型语言。


    1. 编译

    之前我们介绍了dofile 来执行代码块,但是dofile 只是一个辅助函数。这里介绍一下loadfile 函数,它会从一个file中加载语句块,但是不运行;而是仅仅编译并作为一个函数返回。loadfile 不会像dofile 那样在运行时直接报错退出,而是返回错误码,这样我们就可以根据错误码做相应的处理。我们可以像下面这样定义dofile ,这也可以看出dofileloadfile 的区别

    function dofile (filename)
        local f = assert(loadfile(filename))
        return f()
    end

    注意,assert 可以使loadfile 发生错误时,报错退出。


    dofile 在处理有些简单的任务时,使用起来比较方便,只需要一次调用,它会完成所有的操作(编译,运行啥的)。但是,loadfile更加灵活。发生错误的时候,loadfile 会返回nil + 'err_msg',我们可以根据实际情况对错误做出相应的处理。除此之外,如果想要运行一个file多次,可以先调用一次loadfile ,然后调用loadfile 返回的结果多次,就可以了。这比调用几次dofile 开销要小很多,因为loadfile 只会执行一次编译,而dofile 每次都用都要编译。


    loadstringloadfile 差不多,区别是从一个string中加载代码块,而不是从file中。例如:

    f = loadstring("i = i + 1")

    floadstring 的返回值,应该是function类型, 调用的时候,会执行i = i + 1 : 

    i = 0
    f(); print(i) --> 1
    f(); print(i) --> 2

    loadstring 函数功能非常强大,但是运行起来,开销也不小,并且有时会导致产生一些莫名其妙的代码。因此,在用loadstring 之前,先考虑下有没有更简单的办法。

    下面这行代码,不太好看,但是很方便。(不鼓励这种调用方法)

    loadstring(s)()

    如果有语法错误,loadstring 会返回nil + 类似‘attempt to call a nil value’这样的err_msg。如果想获取更详细的err_msg,那就需要用assert

    assert(loadstring(s))()

    下面这样的使用方式(对一个字面值string使用loadstring),没什么意思,

    f = loadstring("i = i + 1")

    粗略的等价于:

    f = function () i = i + 1 end

    但是,也不是完全相同,且继续往下看。第二种方式的代码运行起来更快,因为它只需要编译一次,而loadstring 每次都需要编译。下面我们来看看,上面两段代码到底有什么不同,如下示例:

    i = 32
    local i = 0
    f = loadstring("i = i + 1; print(i)")
    g = function () i = i + 1; print(i) end
    f() --> 33
    g() --> 1

    g 函数处理的事局部变量i , 而f 函数处理的是全局变量i ,loadstring 总是在全局环境中进行编译。


    loadstring 最典型的用途是:运行外来代码,例如网络上的,别人的。。。注意,loadsting 只能load语句,不能load表达式, 如果你想算一个表达式的值,那么前面要加上一个return返回给定表达式的值。下面是一个示例帮助理解:

    print "enter your expression:"
    local l = io.read()
    local func = assert(loadstring("return " .. l))
    print("the value of your expression is " .. func())

    看一下运行情况:(注意第一个为什么报错了,想想什么才叫表达式)


    loadstring 返回的就是一个普通的函数,可以多次调用:

    print "enter function to be plotted (with variable 'x'):"
    local l = io.read()
    local f = assert(loadstring("return " .. l))
    for i=1,20 do
        x = i -- global 'x' (to be visible from the chunk)
        print(string.rep("*", f()))
    end

    string.rep 函数复制一个string给定的次数),下面是运行结果:



    如果我们在深究一下,其实不管loadstring 也好,loadfile 也好,Lua中最基础的函数是loadloadfile 从一个file中加载代码块,loadstring 从一个string中加载代码块,而load 调用一个reader 函数来获取代码块,这个reader 函数分块返回代码块,load 调用它,直到它返回nil 。我们很少使用load 函数;通常只有在代码块不是位于一个file中,但是又太大了,不适合放到内存中(如果适合放到内存中,那就可以用loadstring 了)的时候,才会用load 。


    Lua将每一个独立的代码块看作是一个含有不定数量参数的匿名函数。例如,loadstring("a = 1")跟下面的表达式基本等价:

    function (...) a = 1 end

    跟其他函数一样,代码块也可以声明局部变量:

    f = loadstring("local a = 10; print(a + 20)")
    f() --> 30

    利用这个特性,我们可以重写上面的一个例子:

    print "enter function to be plotted (with variable 'x'):"
    local l = io.read()
    local f = assert(loadstring("local x = ...; return " .. l))
    for i=1,20 do
        print(string.rep("*", f(i)))
    end

    在代码的开始处,加了“local x = ...”,将x声明为局部变量。调用f  函数的时候,实参i 就变成了变参表达式"..."的值。运行结果,跟上面一样,就不截图了。


    load 函数不会发生运行时错误崩溃。如果出错了,总是返回nil+err_msg:


    一个很常见的误解:将load代码块与定义函数划等号。在Lua中,函数定义实际上是赋值行为,而且是在运行时才发生,而不是在编译时。例如,我们有个文件foo.lua:

    function foo (x)
        print(x)
    end

    接着运行下面这条cmd:

    f = loadfile("foo.lua")

    这个时候,foo被编译了,但是还没有被定义。要定义它,必须运行这个代码块:

    print(foo) --> nil
    f() -- defines 'foo'
    foo("ok") --> ok

    在生产环境中的程序,在运行外部代码的时候,要尽可能的捕获所有的错误并作出处理。甚至,如果这些代码不被信任,那就应该在一个安全的环境中运行,避免在运行这些外来代码的时候,产生一些不愉快的事情。


    2. C代码

    不像Lua代码,C代码必须先跟程序链接才能被使用。在大多数系统中,做这个链接动作的做简单的方法是:使用动态链接功能。那么怎么检测你的环境是否已经支持这个功能呢?

    运行 print(package.loadlib("a", "b"))。如果出现类似说文件不存在这样的错误,那么就说明你的环境支持这个动态链接功能啦。否则,出现的错误信息会告诉你这个功能不支持,或者没有被安装。下面是我的环境的表现:


    package.loadlib 有两个string类型的参数,第一个是库的完整路径,第二个是函数的名字。因此,一个典型的调用应该跟下面很相似:

    local path = "/usr/local/lib/lua/5.1/socket.so"
    local f = package.loadlib(path, "luaopen_socket")

    loadlib 函数,加载一个给定的库,并链接,但是并没有去调用那个函数。而是将这个C 函数当作Lua函数返回。如果这个过程出现什么错误,那么loadlib 会返回nil + err_msg。


    要使用loadlib 函数,我们必须要给出库的完整路径和准确的函数名字,好像有点麻烦。这里有个替代方案,require 函数。我们后面在讨论这个函数,这里只需知道有这么个东西就可以了。


    3. 错误

    是个人就难免会犯错。因此我们要尽可能的处理可捕获的错误。因为Lua是一个扩展语言,通常是被嵌入到别的程序(姑且叫做宿主程序吧)中,在出错的时候,不能简单的让它崩溃掉或者退出。而是结束执行当前的代码块,并返回到宿主程序中。

    Lua遇到任何非期望的条件,都会产生一个错误。例如,对非数值进行加运算,调用一个非函数的值,索引一个非table的值,等等。可以显式地调用error 函数来产生一个错误。例如:

    print "enter a number:"
    n = io.read("*number")
    if not n then error("invalid input") end

    if not condition then error end,在Lua中被封装成了assert 函数:

    print "enter a number:"
    n = assert(io.read("*number"), "invalid input")

    assert 检查它的第一个参数,如果为nil或者false,就产生一个error。第二个参数是可选的。


    在函数发现错误时,有两种处理方式,一个是返回error code,另一个是产生错误(联想下C语言中的assert)。如何选择呢?建议如下:可以轻松避免的错误,可以通过编码来修改并规避的,产生错误;否则返回errcode。

    举个例子,sin 函数,如果参数用了一个table,假设它返回了一个error code,如果我们需要去检查一下这个错误,那么代码应该像下面这样写:

    local res = math.sin(x)
    if not res then -- error?
        <error-handling code>

    但是,实际上,我们可以在调用sin 函数之前就检查一下x 是否合法:

    if not tonumber(x) then -- x is not a number?
        <error-handling code>

    如果参数x不是一个数值,那么意味着你的程序中某个地方出错了。这种情况下,停止运行并产生一个错误信息,是最简单有效的方式来处理这个错误。


    我们再来看下io.open 函数,当我去open一个并不存在的file时,会怎么样呢?这个情况,并不能提前检查这个file是否存在,因为在很多系统中,要想知道某个file是否存在,只有去尝试打开它。因此,如果函数io.open 因为一些外部原因(例如file does not exist, permisson denied)而不能打开一个file,它会返回nil + err_msg。这样的话,我们就可以进行一些处理,例如要求用户重新输入一个文件名:

    local file, msg
    repeat
        print "enter a file name:"
        local name = io.read()
        if not name then return end -- no input
        file, msg = io.open(name, "r")
        if not file then print(msg) end
    until file

    如果仅仅希望保证io.open 能够正常工作,可以简单的使用:

    file = assert(io.open(name, "r"))

    这是Lua中的一个习惯用法,如果io.open失败了,就会产生一个错误。

    4. 错误处理和异常

    对大多数程序来说,不需要在Lua代码中进行错误处理,宿主程序本身会对错误进行相应处理。所有的Lua动作基本都是由宿主程序调用起来的,如果发生错误,Lua代码块只需要返回相应的err_code,宿主程序本身针对err_code做出相应的处理。在独立的Lua解析器中,出错的时候,也只是打印出相应的错误信息,然后继续提示用户继续进行运行其他的命令。


    如果想要在Lua中对错误进行处理,那么必须用pcall (protected call)函数来封装代码。


    假设我们运行一段lua代码,并捕获到运行过程中出现的错误。我们首先要做的就是封装这段代码,假设封装成函数foo

    function foo ()
        <some code>
        if unexpected_condition then error() end
        <some code>
        print(a[i]) -- potential error: 'a' may not be a table
        <some code>
    end

    然后用pcall 去调用这个函数:

    if pcall(foo) then
        -- no errors while running 'foo'
        <regular code>
    else
        -- 'foo' raised an error: take appropriate actions
        <error-handling code>
    end

    上面的foo 函数也可以替换成匿名函数的。


    pcall 会在protected mode下调用它的第一个参数,以便能够在函数运行过程中捕获到出现的错误。如果函数运行正常,没有错误产生,pcall 返回true + 函数的返回值;如果出现错误,pcall 返回false + err_msg。

    err_msg不一定非得是string,任何传递给error 的值都会被pcall 返回,例如下面的示例:

    local status, err = pcall(function () error({code=121}) end)
    print(status, err.code, type(err)) 

    运行结果如下:



    5. 错误信息和堆栈

    像上面说的,任何类型的值都可以作为err_msg,但是,通常err_msg还是string类型的,说明发生了什么错误。当遇到了内部错误(例如试图索引一个非table值),Lua负责产生err_msg;否则,err_msg就是传递给error 函数的值。另外,Lua总是在错误发生的地方添加一些位置信息, 如下示例:

    local status, err = pcall(function () a = "a"+1 end)
    print(err)

    local status, err = pcall(function () error("my error") end)
    print(err)

    位置信息指明了filename和line number。


    error 函数有一个额外的参数level, 说明如何获取错误发生的位置。level默认为1,返回error 函数被调用的位置; level 为2, 返回调用error 函数的函数被调用的位置; level 为0,不获取位置信息。对比下下面三段代码的的区别和执行结果就明白了。

    function foo(str)
        if type(str) ~= "string" then
            error("string expected", 0) --level 0
        end
        print("foo success")
    end
    
    local status, err = pcall(foo(3))
    print(err)

    运行结果:


    function foo(str)
        if type(str) ~= "string" then
            error("string expected", 1) --level 1
        end
        print("foo success")
    end
    
    local status, err = pcall(foo(3))
    print(err)

    运行结果:


    function foo(str)
        if type(str) ~= "string" then
            error("string expected", 2) --level 2
        end
        print("foo success")
    end
    
    local status, err = pcall(foo(3))
    print(err)

    运行结果:


    err_test03.lua, err_test04.lua是我code文件的名字而已,可能每个人起名字都不同的。注意一下level 1 和level 2的错误行号是不同的。通过这3个例子,很明白了吧。


    通常情况下,在程序出错的时候,可能仅仅知道错误发生的位置是不够的。至少,还需要函数的调用堆栈吧。但是当pcall 函数返回的时候,堆栈信息已被部分破坏了。因此,为了获取堆栈信息,必须在pcall 返回之前就建立它。Lua为我们提供了xpcall 函数, 它比pcall 函数多一个参数 error handler function。 一旦发生错误,Lua在堆栈被损坏之前调用这个handler 函数,在handler 中,我们可以用debug 库来收集尽可能的有关错误的信息。两个常用的handler 函数是debug.debugdebug.traceback ;具体用法,后续会详细讨论。


    终于写完这篇了,关电脑睡觉去。


    水平有限,如果有朋友发现错误,欢迎留言交流

  • 相关阅读:
    Iconfont在Vue中的使用
    yarn安装依赖报错
    动漫
    伤痛的魅力。绷带男子特辑
    记STM32F103C8T6+STLINK下载器在Keil中的设置
    JQuery浮动对象插件
    树莓派RTL8723BU_LINUX驱动安装
    python虚拟环境相关设置备忘
    解决树莓派控制台命令行乱码
    python模块wifi使用小记
  • 原文地址:https://www.cnblogs.com/xinyuyuanm/p/3030545.html
Copyright © 2011-2022 走看看