LPC 语言基础教程:6.1 LPC语言中的函数类型(匿名函数)

本文节选自《LPC语言基础教程:从零学习游戏开发》,版权归@mudren,欢迎转载,但必须注明来源(mud.ren)。

function类型与匿名函数

在前面的章节我们讲过函数,具体介绍如下:

函数是一段可以重复执行的代码(语句块),并为这段代码取一个特定名字(函数名);通过使用这个特定的名字,你就可以随时随地地调用这个语句块了。

dataType  functionName( dataType1 param1, dataType2 param2 ... ){
    // body
}

在LPC中除了以上函数的用法外还有一种称为 function (函数) 的数据类型。这种类型的变量可以用来指向各种不同的函数,并在需要时调用执行。可以使用以下语法声明函数类型变量:

function functionName = function( dataType1 param1, dataType2 param2 ... )
{
    // body
};

注意最后不要漏了;,因为这一个变量声明语句。

从代码可以看出,这种方式我们取消了函数名,直接固定使用function(...){},改为把函数语句块赋值给变量名functionName,这种没有名称的函数称为匿名函数或者lambda 函数。任何普通函数合法的写法,都可以用于匿名函数。

普通函数我们是通过函数名functionName(param...)来调用函数的,而对匿名函数则是使用以下方式调用:

(*functionName)(param...);

注意和普通函数调用的区别,变量前加一个*并使用()括起来后再调用,如果学过C语言会很熟悉:*functionName这就是对指针类型变量取值的操作方式。

我们看以下示例:

void create() {
    function f = function(int x) {
        int y;

        switch(x) {
        case 1: y = 3;break;
        case 2: y = 5;
        }
        return y - 2;
    };

    printf("%i %i %i\n", (*f)(1), (*f)(2), (*f)(3));
}

会输出:

1 3 -2

提示:对匿名函数形式的变量如果你直接write(functionName)会输出:(: <code>() :)

除了使用 (\*f)(...),还可以直接使用evaluate(f, ...)这个外部函数调用匿名函数,前一种方式有经验的程序员可能看着感觉更习惯,但目前MUD开发中多数还是使用evaluate(f, ...)的方式。

evaluate() 外部函数具体语法说明如下:

名称

evaluate() - 执行一个函数指针

语法

mixed evaluate(mixed f, ...)

描述

如果 `f` 是一个函数会使用剩余的参数调用 `f`,否则直接返回 `f`。 evaluate(f, ...) 的作用和直接调用  (*f)(...) 相同。

注意evaluate()的参数,除了可以是函数还可以是其它类型,这在某些情况下会更灵活(后面有示例)。

对匿名函数我们可以赋值给函数类型的变量然后调用,也可以直接做为参数传给其它函数使用,如:

void test()
{
    printf("%O\n", map_array(({1, 2, 3, 4, 5}), function(int x) { return x * 10; }));
}

PHP及Javascript程序员应该会发现,LPC语言中匿名函数的写法和用法和这两种语言非常相似。

函数类型判断

基本上每种数据类型都有一个efun可以用来判断,对function类型也有一个外部函数int functionp( mixed arg );用来判断。如果是函数类型返回非0值,具体定义在<function.h>中:

/* codes returned by the functionp() efun */

#define FP_LOCAL 2
#define FP_EFUN 3
#define FP_SIMUL 4
#define FP_FUNCTIONAL 5

/* internal use */
#define FP_G_VAR 6
#define FP_L_VAR 7
#define FP_ANONYMOUS 8

/* additional flags */
#define FP_MASK 0x0f
#define FP_HAS_ARGUMENTS 0x10
#define FP_OWNER_DESTED 0x20
#define FP_NOT_BINDABLE 0x40

最后一组值为位元值(bit values),可以用来做位运算测试,FP_MASK 可以忽略某些位测试函数指针的基本型态。

测试一个函数指针是否是外部函数(efun)指针:

if ((functionp(f) & FP_MASK) == FP_EFUN) {}

测试函数是否有参数:

if (functionp(f) & FP_HAS_ARGUMENTS) {}

函数指针

PHP和Javascript中对匿名函数还有一种更简洁的写法:箭头函数(箭头函数是 PHP 7.4 的新语法),而在LPC中也有一种和函数相关的简洁写法(: func :)可以赋值给function类型变量,不过这不是对匿名函数的简洁,而是可以把有名函数赋值给函数类型的变量和做为函数参数调用

函数实际上是对象,函数名是指向函数对象的指针,指向匿名函数的function类型变量也是函数指针。为了方便介绍和区分匿名函数形式,我们统一把(: func :)这种形式称为函数指针。下面我们根据示例具体介绍。

函数指针定义的具体格式如下:

    function f = (: function_name :);
    function f = (: function_name, args... :);
    function f = (: expression :);

如下示例:

void debug(mixed arg)
{
    debug_message(sprintf("\e[%dm%O\e[0m", 31 + random(6), arg));
}

void create()
{
    function f = (: debug :);

    for (int i = 0; i < 3; i++)
    {
        evaluate(f, "Hello world!");
    }
}

把函数debug()赋值给函数类型变量f:

function f = (: debug :);

稍后再执行 f:

evaluate(f, "Hello, world!"); 
(*f)("Hello, world!");

执行时,会呼叫 f 指向的函数debug(),而 "Hello world!" 当成参数传入函数。其效果同下:

debug("Hello world!");

函数指针也可以在赋值时传函数的参数,如:

void create()
{
    function f = (: debug, "Hello world!" :);

    for (int i = 0; i < 3; i++)
    {
        evaluate(f);
    }
}

甚至可以在函数类型赋值时传部分参数,在调用时传剩余参数。例如:

string foo( string a, string b ) {
   return "(" + a + "," + b + ")";
}

void create() {
    function f = (: foo, "left" :);

    printf( "%s %s\n", evaluate(f), evaluate(f, "right") );
}

会输出:

(left,0) (left,right)

提示:和匿名函数形式不同,对函数指针形式的变量如果你直接write(functionName)会输出具体内容,如:(: debug, "Hello world!" :)

使用函数指针(: function_name :)格式时function_name可以是任何类型的函数,在前面章节我们讲过LPC语言中的函数和方法包括四类:局部函数(local function)、外部函数(efun)、模拟外部函数(sefun)和apply方法,这些都可以用(: function_name :)调用,只是在函数指针中使用局部函数和使用普通有名函数一样,必须是当前对象中定义的,而且必须在调用前定义,否则会报错。建议如果一个局部函数功能很简单且只是做为函数参数调用一次可以直接改用匿名函数实现。

在游戏中最常用的一种函数指针应该是对 call_other() 做函数指针的使用,我们知道call_other()外部函数有以下几种语法:

unknown call_other( object ob, string func, ... );
unknown call_other( object ob, mixed *args);
unknown call_other( string file, string func, ... );
unknown call_other( string file, mixed *args);
unknown call_other( object *obs, string func, ... );
unknown call_other( object *obs, mixed *args );

其中如果 call_other 函数的第二个参数是数组,那么数组的第一个元素必须是要调用的方法名字符串 func ,剩余的元素则是要传入方法的参数。所以我们可以这样用:

function f = (: call_other, ob, func, args... :);
function f = (: call_other, ob, ({func, args...}) :);

当然,除了以上2个示例还有其它格式,函数指针调用形式是看具体函数可以传的参数形式,不用迷惑不解,如果不明白,多看看函数原型支持的传参。

使用函数指针的好处是,如果您想使用不同的函数,只需要改变函数指针变量的值,当然,更重要的是可以做为参数用于其他方法或外部函数中。和匿名函数一样,函数指针(: func :)可以直接做为参数在其它函数中直接使用,而不需要先赋值给函数类型变量,如:

object *ob = objects( (: clonep :) );

直接使用clonep这个efun做参数,结果传回游戏中所有的复制对象 (clones)。

请注意在函数参数中(: func :)func()的区别:(: func :)是function类型的参数,而且能接受函数的传参,而func()则是把函数执行的结果做为参数。如上示例如果代码写成object *ob = objects(clonep());就出错了,这个本质上是clonep()的结果(0或1)传给了objects()函数。

最后补充一下前面我们讲过 evaluate() 的灵活使用示例,因为evaluate()参数如果不是函数,就只会传回参数值。所以您可以做以下的事:

void set_short(mixed x)
{
    short = x;
}

mixed query_short()
{
    return evaluate(short);
}

这样,简单的对象可以只用 set_short("Whatever"); 以达成 set_short( (: short_func :) ); 的效果,这也是开发中用evaluate(f)代替(*f)()的原因,不过,如果你只是调用函数指针,还是推荐使用(*f)()的方式,毕竟更简洁。

表达式函数指针

函数指针除了可以调用函数外,还可以是表达式 (expression), 就是 (: 表达式 :)。在一个表达式函数指针中,参数可以用 $1、$2、$3 ... 代表,举例如下:

evaluate( (: $1 + $2 :), 3, 4)  // 传回 7.

这可以用于 sort_array,范例如下:

top_ten = sort_array( player_list, (: $2->query_level() - $1->query_level :) )[0..9];

以下示例也是表达式形式:

function f = (: this_player()->query("name") :);
object *ob = objects( (: !clonep($1) :) );

函数指针中的局部变量

注意:不可以在表达式函数指针里使用局部变量,因为执行这个函数指针之后,这个局部变量就不存在了,但是可以用下面这个方法解决:

(: destruct( $(this_player) ) :)

$(whatever) 表示要执行 whatever,并保留其值。当执行此函数时,再传入这个值:

map_array(listeners, (: tell_object($1, $(this_player()->query_name()) + " bows.\n") :) );

只做一次 call_other ,而不需要每个讯息都做。也可以事先合并字串:

map_array(listeners, (: tell_object($1, $(this_player()->query_name() + " bows.\n")) :) );

注意,在这个情形下,也可以这样做:

map_array(listeners, (: tell_object, this_player()->query_name() + " bows.\n" :) );
京ICP备13031296号-4