函数

一、函数简介

函数首先是一个子程序,可通过这段子程序计算并返回一个或多个值。

1、函数首先是作为一个子程序,封装一段可复用的代码。
所以我们可以把一段使用频繁的代码写到函数中。然后在需要使用时调用函数就行了。

2、函数可以接授一个或多个参数,计算并返回一个或多个值。

--用function语句定义函数,end;语句表示函数结束。括号里声明参数名字

function test(a,b)  --用括号指定形参(参数列表)
   return a+b,"哈哈"; --函数中用return语句返回一个或多个值
end;--语句表示函数结束

c,msg = test(2,3); --括号内部指定实参(传递给函数参数的数据),
--c的结果为5,msg的结果为"哈哈"

与其他编程语言不同的是,在LAScript函数可以有多个返回值。

二、定义函数

定义函数的基本语法

function 函数名字(参数名字列表)
       -- 一系列的代码
       return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end;

因为函数也是变量,所以我们也可以通过变量赋值语句定义函数。

函数名字 = function(参数名字列表)
       -- 一系列的代码
       return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end;

调用函数很简单

变量列表 = 函数名字(实际参数列表);
函数名字(实际参数列表); --如果不需要返回值,可以这样写
函数名字 "参数";--如果只有一个字符串参数,可以不使用括号。
函数名字(); --如果没有参数,也要用括号。

 形参:函数定义时括号中指定的参数名字列表。
 实参:函数调用时括号中包含的实际数据列表。

function test(a,b,c)  --这里的a,b,c称为形参, 可以将形参看成函数内部的局部变量名字。
   return a+b+c;
end;

c,msg = test(2,3,4); --这里的2,3,4 称为实参

实参的数目如果多于形参的数目,多余部份被丢弃。
实参的数目如果少于形能的个数,不足的部份添加nil值。

三、函数局部变量、变量作用域

用local语句声明只能在函数内部使用局部变量,
函数中的局部变量与全局变量命名相同时,使用局部变量,二者并不冲突,各自有自已的作用域

str = "a"

function func()
    local str = "b"
    win.messageBox(str) --显示局部变量值: "b"
end;

func();
win.messageBox(str) --显示全局变量值: "a"

函数的参数是一个局部变量。参数与全局变量命名相同时,使用参数值,二者并不冲突,各自有自已的作用域
str = "a"

function func(str)
    win.messageBox(str) --显示参数值:"参数"
end;

func("参数");
win.messageBox(str); -- 显示全局变量值: "a"
如果在函数中的变量没有用local语句声明的变量,使用全局变量
str = "a"

function func()
    str = "b"
    win.messageBox(str) --显示全局变量值: "b"
end;

func();
win.messageBox(str) --显示全局变量值: "b"


在函数中使用全局变量并不安全,因为全局变量大家都可以访问,定义函数时无法预期到全局变量什么时候会被修改或删除。
尽可能不要在函数中使用全局变量!




四、局部函数

函数可以作为全局变量也可以作为局部变量。局部函数像局部变量一样仅在作用域内有效。

定义局函数的方法一

local function 函数名字(参数名字列表)
       -- 一系列的代码
       return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end;

定义局函数的方法二

local 函数名字 = function(参数名字列表)
       -- 一系列的代码
       return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end;


声明为局部函数的递归问题。
 
如果在局部函数中调用自身,因为局部函数定义时并不知道自已是一个局部函数,这样就会在全局表中找不到自已。

local dg = function (a)
    if a <= 0 then
        return a
    else
        return dg(a-1)   -- 出错了找不到全局函数dg
    end
end
   
win.consoleOpen()
print(dg(5) )
 


下面是正确的写法
local dg;
dg = function (a)
    if a <= 0 then
        return a
    else
        return dg(a-1)   -- 正确,已经声明了局部变量dg
    end
end
   
win.consoleOpen()
print(dg(5) )
 

五、为table类型的变量定义成员函数

因为函数也是变量,所以函数可以作为table成员,我们也可以通过变量赋值语句定义成员函数。

tab = {};
tab.函数名字 = function(self,参数名字列表)
    -- 一系列的代码,如果需要引用函数所在的table,可以添加一个self参数。
    return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end

--调用函数的方法
tab.函数名字(tab,参数列表);--将tab自身传递给self参数。

我们还可以用下面的方法定义成员函数,作用完全相同。

tab = {};
function tab.函数名字(self,参数名字列表)
    -- 一系列的代码,如果需要引用函数所在的table,可以添加一个self参数。
    return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end

--调用函数的方法
tab.函数名字(tab,参数列表);--将tab自身传递给self参数。

上面的示例中,self参数是可选的,并非必需,如果一定要添加self参数,可以使用冒:号来声明或调用函数。
冒:号放在函数名前面时会隐式的传递一个self参数(隐含的意思是不需要在参数列表中声明、赋值self参数)

tab = {};

function tab:函数名字(参数列表)
    win.messageBox(""..type(self));--默认会添加一个隐藏的self参数表示table变量自身
    return 返回值列表; --如果省略,则会在这里默认添加一个return nil;语句返回空值。
end;

--调用函数的方法
tab:函数名字(参数列表);

如果需要访问表自身应当使用self参数而不要使用外部变量名,隐含或显示的使用self参数都可以。
因为函数不知道外部变量名什么时候被删除或者指向别的对象。

错误的用法:

tab = {x=0};

function tab.func()
    return tab.x; --这里会出错,表仍然存在,但是tab这个名字已经不存在了
end;

tab2 = tab;
tab = nil;
tab2.func();

正确的用法

tab = {x=0};

function tab.func(self)
    return self.x; --正确的指向实际的表
end;

tab2 = tab;
tab = nil;
tab2.func(tab2);

如果你喜欢添加一个参数,还可以通过在函数名前使用冒号隐含的传递self参数。
这也是在LAScript中应用最广泛的一种写法。

tab = {x=0};

function tab:func()
    return self.x; --正确的指向实际的表
end;

tab2 = tab;
tab = nil;
tab2:func();

 

六、使用函数的返回值

--定义一个函数
gethw = function()
     return "hello","world";--这个函数有两个返回值
end;
 
--用多个变量名匹配多个返回值
h,w = gethw(); --用两个变量名匹配返回值
--结果:等于"hello",w等于"world"
 
--增加变量名只能取到nil值
h,w,w2 = gethw(); --变量名的个数多于返回值个数,多余的变量赋值为nil
--结果:h等于"hello",w等于"world",w2等于nil
 
-- 减少变量名丢弃不需要的返回值
h = gethw(); --变量名的个数少于返回值个数,多余的返回值被丢弃
--结果:h等于"hello",
 
--不需要的返回值可以用_占位符替代变量名。
_ ,w =gethw(); --用一个_占位符丢弃相应的返回值
--结果:w等于"world"
 
-- 括号中的表达式总是返回一个值
h =( gethw() ); --用括号强制取函数的第一个返回值,丢弃其他的返回值
 
--多个返回值也可以放在table构造器中
tab = { gethw() }; --把所有的返回值放到表构造器中生成一个tab
--结果:tab[1]等于"hello",tab[2]等于"world"
 
--select函数能在多个返回值中取出指定位置的返回值
w = select(2 , gethw() ); --将test的所有返回值作为其他函数的参数
--select函数将指定位置(用第一个参数加一)的参数作为返回值
--结果:w = "world"
 
-- 多个返回值可以直接作为其他函数的参数
print( gethw() );--将test的所有返回值作为其他函数的参数
-->输出 hello world
 
--将函数的返回值放在函数的其他参数前面,仅使用第一个返回值
print( gethw(),"123" );
-->输出 hello 123 , 注意world被丢弃了
 
--将函数的返回值放在函数的其他参数后面, 使用所有返回值
print( "123",gethw());
-->输出 123 hello world 
 

七、使用函数的参数

  1. 按值传递参数、按引用传递参数

    函数的参数遵循赋值语句的规则,对于普通类型拷贝一个副本按值传递。
    function set(x) --参数是按值传递的 
       x = 200;--改变x不会改变外部变量的值     
    end;
     
    x = 100;
    set(x);--参数是按值传递的
     
    win.consoleOpen();--打开控制台窗口
    print(x); -->还是100
    对于table,function,userdata类型的参数,是按引用传递的。
    如果在函数内部对参数赋值指向其他对象,仍然不会改变外部变量的值。
    function set(cl) --参数是按引用传递的 
       cl = 256;--仍然不会改变外部变量的值,因为cl不再指向cl2而是存储数字值256
    end;
     
    cl2 = color(123);
    set(cl2);--userdata参数是按引用传递的
     
    win.consoleOpen();--打开控制台窗口
    print(cl2); -->cl2还是一个color对象,而不是数值256
    对于table,userdata类型的参数,如果在函数内部不改变参数的指向,那么可以改变外部变量的成员变量的值。
    function set(t) --参数是按引用传递的 
      t.x = 256;--在这里成功改变外部变量的值
    end;
     
    tab ={x=10,y=20}
    set(tab);--table参数是按引用传递的
     
    win.consoleOpen();--打开控制台窗口
    print(tab.x); -->显示256,tab.x被函数改变了
    如果您需要在函数内部改变外部变量的值,那么您应当把数据包装到一个table变量中。


  2. table在函数参数中的应用

    上面我们已经介绍了,将变量封装到table中,可以在函数内部改变函数外部变量的值,因为table是按引用传递的;


    function set(t) --参数是按引用传递的 
      t.x = 256;--在这里成功改变外部变量的值
    end;
    tab ={x=10,y=20};--变量封装到table中作为参数
    set(tab);--table参数是按引用传递的
     
    将参数封装在table中还有一个好处,如果有很多的参数,又要省略一些参数时-不需要用实参一个个的匹配形参的位置。
    win.consoleOpen();
     
    function test(a,b,c,d,e,f,g,h,i,j,k,l,m)
       print(i,m);
    end
     
    --假设上面的函数中所有的参数都可以省略,只有i和m是不可以省略的。
    --那么我们可能要这样写
    test(nil,nil,nil,nil,nil,nil,nil,nil,2,nil,nil,nil,3);
    因为实参与形参是按位置匹配的,所以最后的可省略参数可以直接省略。
    如果在可省略的参数后面还有其他的参数要写,我们只有通过赋值为nil表示省略。


    下面是更好的方案:
    -- 下面我们用table来封装参数实现同样的功能,是不是简单的多了呢?
    function test(t)
       print( t.i , t.m );
    end
     
    tab ={i=2,m=3}
    test(tab);


    分离table的所有成员并作为函数的参数
    win.consoleOpen();
     
    function set(x,y) 
      print(x,y);
    end;
     
    tab ={10,20}
    set(unpack(tab));
     
    --上面的代码等效于
    set(tab[1],tab[2]);

  3. 在函数中使用可变参数

    三个连续的圆点表示可变参数,使用函数内部的arg列表可以索引所有的参数。


    win.consoleOpen();
     
    function set(...) -- 三个连续的回点表示任意个数、任意类型的参数
      print( table.maxn(arg) );-->显示可变参数的个数
      print(arg[1],arg[2]); -->显示第一个可变参数,第二个可变参数
    end;
     
    set(12,23);

    其实,我们经常使用的pint函数就是接收可变参数的,可以给print传递任意类型任意个数的参数。
    在可变参数前面可以添加固定名称的参数,例如:
    win.consoleOpen();

    function
    set(x,y,...) -- 三个连续的回点表示任意个数、任意类型的参数
      print( table.maxn(arg) );-->显示可变参数的个数(不包括前面的固定名称的参数)
      print(arg[1],arg[2]); -->显示第一个可变参数,第二个可变参数
    end;
     
    set(x,y,12,23);
    可变参数被存储在arg列表中,arg列表是一个table变量.有时候我们需要展开arg表得到多个值,例如展开arg作为其他函数的参数。
    我们可以使用unpack函数展开arg表。下面是一个例子。
    function print2(...)
        print( unpack(arg) ); --unpack可以接受一个table数组作为参数, 分离数组并一个个的放到返回值中。.
    end

    print2(12,23,"abc")





    注意
    如果函数没有使用可变参数,则不会创建局部变量arg。但是每个fap程序或LAS脚本都有一个默认的全局变量arg,存储命令行参数。用下面的代码可以检视所有命令行参数。

    win.consoleOpen();

    for k,v in pairs(arg) do
       print(k,v);
    end


  4. 函数也可以作为参数

    函数本身也是一个变量,也可以作为参数,例:

    function test()
       win.messageBox("我是test")
    end
     
     
    function func( f )
    win.messageBox("我是func,下面我执行我接收到的函数f()")
    f();
    end;
     
    func(test);--将函数test作为参数传递给了func函数

八、闭包、作用域

一个函数可以在内部包括另一个函数。
函数内部的函数如果加上local语句则定义的函数仅能在函数内部使用。

1、函数外部不能访问函数内部的局部变量。
2、内部函数可以访问外部函数的局部变量。
3、每次执行函数都会定义新的内部函数,创建新的闭包。闭包在函数结束后仍然存在(直到没有指向闭包内部函数的引用而被回收)。
4、闭包中的局部变量能在函数结束后保持其值。闭包中的内部函数可以访问闭包中的局部变量。

如果与c++中的类、对象机制进行比较。

LAScript中的table即是类(可以声明结构)又是对象(可以存储数据),拥有公开成员,但是却没有构造新对象的默认方法(可以通过std库中的class函数实现这个功能)。

LAScript中的函数虽然没有公开成员,但是却可以构造新的对象(闭包),函数是没有公开成员的闭包是没有公开成员的对象内部函数可以看作这个对象的公用接口闭包中的局部变量可以看作是这个私有成员

闭包实际上并不是一个作用域,也不是函数,更不是语句块,而是函数被执行时创建的一个新的对象。在函数结束后仍然存在


win.consoleOpen();
 
function upfunc2()
 
    local upfunc = function()
        local upv = 23;

        function func() --函数里面可以包括函数
            print(upv); -->23,upfunc函数的局部变量在他的内部函数func中同样有效
        end

        func();-->可以调用当前作用域中已经定义的函数

    end;

    print(upv);-->nil 在upfunc外部不能访问upfunc内部的变量
    upfunc();-->可以调用当前作用域中已经定义的函数

end;
 
upfunc2();


在函数内部的函数,如果不在函数定义前面加上local语句,实际上等于定义了一个全局函数。
但是他同样遵循闭包的规则,可以访问定义他的外部函数中的局部变量。

win.consoleOpen();
   
function upfunc()
    local upv = 23;
    function func() --函数里面可以包括函数
       print(upv); -->23,upfunc函数的局部变量在他的内部函数func中同样有效
       upv = upv+1;
    end
   
end;
 
 
upfunc();
--这句代码会执行 upfunc内部的 local upv = 23; 同时upv被初始化为23
 
--在执行upfunc()也同时定义了 func函数
--注意,如果不执行外部函数则函数内部的下层函数是不存在的。
--因为func函数没有加上local语句,所以成为全局函数,可以直接使用了
func(); -->显示23,通过闭包规则访问upfunc函数中的局部变量并将其加1
func(); -->显示24,可以看到upv已经被加1了
func(); -->显示25,可以看到upv已经被加1了
func2=func; -->将第一次调用upfunc()创建的函数存储到一个新的变量中
 
upfunc();
--再次执行upfunc(),创建了一个新的func函数
func(); -->显示23,每次调用外部函数都会创建新的闭包,在这个闭包里 upv有自已独立的值
func(); -->显示24,可以看到upv已经被加1了

func2(); -->显示26,可以看到每次调用upfunc()都会创建新的闭包,各自有自已独立的局部变量
-->func2的闭包仍然独立存在,并不会与新建的func共享一个闭包


函数每次执行都会创建新的闭包。我们再看一个例子:
下面的函数与上面的函数执行的效果完全相同。将内部的函数作为返回值,强制我们为每次创建的内部函数赋于一个新名字。

win.consoleOpen();
 
function upfunc()
    local upv = 23;
    local func = function() --与上面的函数不同func加了local语句被声明为局部函数
        print(upv); -->23,upfunc函数的局部变量在他的内部函数func中同样有效
        upv = upv+1;
    end
   
    return func;--将内部函数作为返回值,强制我们给每次创建的函数一个变量名
end;
 
--上面的函数定义也可以用下面的写法:

function upfunc()
    local upv = 23;
    return function() --返回一个"匿名函数",LAScript可以创建没有名字的函数赋值给其他变量
        print(upv); -->23,upfunc函数的局部变量在他的内部函数func中同样有效
        upv = upv+1;
    end
end;

 
f2 = upfunc(); -->这样我们必须给func函数一个新的名字了
 
f2(); -->显示23,通过闭包规则访问upfunc函数中的局部变量并将其加1
f2(); -->显示24,可以看到upv已经被加1了
f2(); -->显示25,可以看到upv已经被加1了
 
 
f3 = upfunc();
--再次执行外部函数,同时upv被初始化为23
 
f3(); -->显示23
f2(); -->f2的闭包仍然独立存在,并不会与f3共享一个闭包

九、尾调用

请看下面的例子:

function f (x)
     return f2(x)
end

如果一个函数在调用另外一个函数以后不再做任何事称为尾调用。
尾调用不会返回原来的函数(类似goto),所以不需要额外的栈保留调用函数的数据。

正确的尾调用
1、必调用必须是在最后一个return语句中
2、return语句后面只能有一个函数调用,不能使用其他表达式
例如 return g(x) + 1 不是尾调用
3、参数中可以使用表达式。

递归调用函数是第浪费资源的,但是如果使用尾调用就需要大量的压栈,
因为无论递归多少次都不会导致栈溢出。

实际上,我们前面例子中的递归函数就是一个典型的尾调用函数


dg = function (a)
    if( (nStop()==false) or (a <= 0) ) then --这里用nStop()检测您有没有按全部停止
        return a
    else
        return dg(a-1)   -- 如果一个函数在调用另外一个函数以后不再做任何事称为尾调用
    end
end
   
win.consoleOpen()
print(dg(99999999999999999999999999999999999) );--因为是尾调用不会导致栈溢出
 

如果我们把 return dg(a-1) 改成 return dg(a-1)+1就不再是尾调用了,你会看到内存不断的增加直到栈溢出。