跳到主要内容
版本:master

函数

1 概述

函数是GS语言中组织代码和实现功能复用的基本单元。本章将介绍GS语言中函数的各种特性和使用方法,包括函数的基本概念、定义方式、参数传递、返回值处理、调用类型、匿名函数以及函数式编程支持等内容。本章面向GS编程初学者或需要回顾GS函数基础知识的同学。通过学习本章,您将能够熟练运用GS函数来构建模块化、可维护的代码结构,了解函数式编程在GS中的应用,并初步知晓各个函数调用方式间的差别。

2 函数的概念

在GS语言中,函数是实现特定功能的代码块,它可以接受输入参数、执行特定操作并返回结果。函数的主要作用包括:

  • ​代码复用​​:将常用功能封装成函数,避免重复编写相同代码

  • ​模块化设计​​:将复杂问题分解为多个函数,提高代码的可读性和可维护性

  • ​抽象化​​:隐藏实现细节,只暴露必要的接口

GS中的函数分为两种类型:

  • ​外部函数(EFun)​​:由GS底层使用C++实现的函数,提供核心功能。

  • ​内部函数​​:使用GS语言编写的函数,可以基于外部函数构建更复杂的功能。

  • 注意:外部函数的作用域是整个源程序,而内部函数只能在它的域(domain)中使用或者被跨域调用。

3 函数基础

3.1 函数的定义

GS普通函数的定义基本语法为:

prefix return_type function_name(argument_list)
{
// 函数体
// 实现特定功能
return value; // 可选
}

普通语法点定义解释如下:

  • prefix 表示类型前缀,它是可选的,默认为private,其类型有以下五种:

    • public- 公有函数,可在任何地方访问

    • protected- 保护函数,只能在当前及子函数中访问

    • private- 私有函数,只能在本文件中访问(默认)

    • virtual- 虚拟函数,支持多态行为

    • override- 覆盖函数,重写基类中的虚函数

  • return_type 表示返回值类型,也就是这个函数执行完之后需要返回值的类型

    • mixed - 所有已有数据类型或者自定义数据类型都可以作为返回值
    • return_type - 是每个函数必须要有,不可省略
    •  void - 如果函数没有返回值,可以标注为 void
  • function_name 是函数的名字

    • 它也是不可省略
    • 不能跟系统函数同名,以免造成冲突
  • argument_list 表示函数的参数

    • 函数的参数可以是零个或多个
    • 参数表中参数的格式为value_type value其中参数之间用逗号隔开
    • 使用 ... 可以接受变长参数
    • 支持以value_type value = const_val的方式指定默认值 const_val
    • 支持使用 readonlyparallel进行限定修饰
  • body_of_general_function 是函数体

    • 函数体是实现函数的最主要模块
    • GS 返回值带有控制流检查,若返回类型不匹配编译器会报错
    • 当函数没有 return语句时或return语句后无变量时默认返回为 void

看起来函数可指定的内容非常多甚至觉得有点复杂,但通常绝大多数函数的声明实际非常简单,一个简单的计算两个输入整型的最大值的函数定义示例如下:

public int max_int(int x, int y) // 计算x和y中的最大值,函数定义为公有函数
{
if(x > y)
return x;
return y;
}
printlnf("Max int for 2,3 is %d", max_int(2,3));
printlnf("Max int for 5,4 is %d", max_int(5,4));

示例3-1: 简单的取最大值函数定义示例

输出结果如下:

Max int for 2,3 is 3
Max int for 5,4 is 5

3.2 函数的参数

函数的参数支持默认参数,支持可变参数,支持使用 readonlyparallel对参数进行限定修饰。

3.2.1 基本参数声明

基本参数声明示例如下:

void simple_function(int num, string text, array data)
{
// 函数实现
}

示例3-2:基本参数传递示例

我们只需要指定参数的类型,参数的名称,将它们放入括号内并用逗号隔开就好。可以自行尝试在函数实现中添加一些简单的输出语句打印函数参数并调用 simple_function

3.2.2 参数默认值

若要为函数的指定默认参数,只需要在基本参数传递的基础上为需要默认值的参数声明后面添加= const_val 语句即可。

默认参数示例如下:

void greet_user(string name = "访客", int count = 1)
{
for(int i = 0; i < count; i++) {
write("你好, " + name + "!");
}
}

示例3-3:简单的默认参数示例

需要注意的是:

  • 为参数指定的默认值必须是编译期可计算的常量

  • 默认参数遵循右连续原则,如果某个参数设置了默认值,那么它右边的所有参数都必须有默认值。

    • // ✓ 正确:默认参数从右向左连续
      void func(int a, int b = 10, int c = 20){};

      // ✗ 错误:默认参数出现间断
      void func(int a = 5, int b, int c = 20){}; // 编译错误

3.2.3 参数限定修饰

若要求检查参数为只读或并行实例,则可以通过readonlyparallel进行限定修饰。不过readonlyparallel限定修饰只适用于非mixed类型以及非intfloat基本类型。

GS支持的限定修饰符如下:

修饰符用途限制
readonly确保参数为只读实例不能用于基本类型和mixed类型
parallel确保参数为并行实例不能用于基本类型和mixed类型

参数限定修饰示例如下:

void foo(readonly map m)
{
// ...
}
foo({}); // 运行时异常,传入的参数并不是 readonly 的
foo(make_parallel({})); // 运行时异常,readonly 的参数只接受 readonly, 反之也一样
foo(make_readonly({})); // Ok

示例3-4:参数限定修饰示例

可以自行修改代码逐个尝试三个foo函数调用并查看代码运行结果。

3.2.4 变长参数

GS函数提供变长参数列表,在函数参数列表中使用 ... 语句表示函数有可变参数。可变参数必须作为函数参数列表的最后且唯一的可变元素。在其后声明的任何其他参数都会导致编译错误。

 可变参数声明示例:

// ✓ 正确:可变参数在末尾
void log_message(string prefix, string level, ...){};
void print_values(...){};

// ✗ 错误:可变参数后还有其他参数
void process_data(..., string format){}; // 编译错误
void collect_args(int count, ..., bool validate){}; // 编译错误

示例3-5:正确的可变参数声明示例

正确的可变参数用法示例如下:

public array ping_args(...)
{
if($?) // 如果有参数
{
if(lengthof($<) == 1) // 如果只有一个参数
return [$1]; // 返回第一个参数
return ($<)[0..<1]; // 否则返回所有参数
}
else // 没有参数
return ["no argument"];
}

示例3-6:正确的可变参数使用

$ 相关参数介绍:

  • $< 表示所有可变参数构成的数组
  • $? 布尔值,表示是否有参数
  • $num 表示第 num 个参数,如 $1 表示第一个参数
  • ($<)[begin_num..<end_num] 从第begin_num个参数(第一个为0)开始到倒数第end_num个(包括)。
    • 比如($<)[0..<1]表示从正数第0个参数开始到倒数第一个参数构成的子数组,也就是所有参数构成的数组

3.3 函数的返回

3.3.1 函数返回类型

GS 支持多种返回值类型,比如voidintarrayhandlemixed 等类型,出了nil这种值类型不可以作为 GS 的返回类型以外,其他的类型均可作为函数的返回值类型。

 GS函数提供多种返回值类型,示例如下:

void test()
{

}

int add(int a, int b)
{
return a + b;
}

array get_arr()
{
return [1,2,3];
}

handle get_file_handle()
{
return this_object();
}

raw_pointer get_raw_pointer()
{
return raw_pointer.gc_allocate(8);
}

示例3-7:GS支持多种返回值类型

3.3.2 函数的返回值

3.3.2.1 return语句

在函数中使用 return;return val;语句结束函数执行并返回函数的执行结果。函数可以返回与定义的返回类型匹配的任何函数类型。

GS 函数实际返回的值类型必须与 GS 函数定义的返回类型匹配,比如定义一个返回array的函数,那么我们在运行时就只能返回arraynil类型的值,若返回值类型不匹配,GS 则会报错。

使用return语句以及返回类型匹配示例如下:

array ret_mixed(mixed ret)
{
return ret;
}

ret_mixed([]); // 正确,返回值类型为 array
ret_mixed(nil); // 正确,返回值类型为 nil
ret_mixed("string"); // 错误,返回值类型为 string. 报错: Error(-1): Can not implict cast 'string' to 'array'

示例3-8:函数返回及类型匹配示例

当函数返回类型被设置为mixed时,函数可以返回任何类型的返回值。

3.3.2.2 无返回值

当函数没有使用return返回语句返回任何值或return;语句的val字段为空时则默认返回 void,示例如下:

void no_ret()
{

}

void ret_no_val()
{
return;
}

int branch_ret_void(int arg) // 错误示例:返回类型不匹配
{
if(arg == 1)
return arg; // 此分支返回整型
else if(arg == 2)
return arg; // 此分支返回整型
// 此分支无返回,返回默认值 void
}

示例3-9:无返回值函数示例

通过上述示例可以看到,当函数没有使用return返回语句返回任何值或return;语句的val字段为空时则默认返回 void

需要额外注意的是示例中的branch_ret_void函数,这是一种常见的错误返回类型的情况。该函数在最后一个可达的分支中返回了void 但规定的返回类型确却是int导致类型不匹配报错。

3.3.3.3 多返回值

GS允许通过返回数组+let语法糖的形式实现近似多返回值的效果,let语句语法为以 let 关键字开头,后跟以逗号隔开的变量或变量声明,后跟数组实例赋值 = array_instance;,形如let var0, var1, var2 = array_instance;let mixed var0, int var1, string var2 = array_instance;示例如下:

array get_multiple_arg()
{
return [1,2,3];
}
let int a, int b, int c = get_multiple_arg();
printlnf("Get multiple return a:%d b:%d c:%d", a, b, c); // 将返回数组元素逐个赋值给变量a,b,c

let a,b,c = [0,0,0];
printlnf("Get multiple assign a:%d b:%d c:%d", a, b, c); // 将数据的元素逐个赋值给变量a,b,c

let a, b, c, int d = get_multiple_arg();
printlnf("Get multiple return a:%d b:%d c:%d d:%d", a, b, c, d);// 返回数组长度为3但有4个变量,变量d 未被赋值

let a,b = [0,0];
printlnf("Get multiple assign a:%d b:%d", a, b);

let a, b = get_multiple_arg();
printlnf("Get multiple return a:%d b:%d", a, b); // 返回数组长度为3但有2个变量,变量 a,b 被成功赋值

示例3-10:多返回值实现

示例的输出结果如下:

Get multiple return a:1 b:2 c:3
Get multiple assign a:0 b:0 c:0
Get multiple return a:1 b:2 c:3 d:0
Get multiple assign a:0 b:0
Get multiple return a:1 b:2

可以看到当数组长度与多赋值变量数量不匹配时,赋值也能正常进行。

  • 当变量数量n 大于数组长度 len时,超过长度的变量未被赋值。

  • 当变量数量n 小于数组长度 len时,数组的前 n 位成员被赋值给对应变量

4 函数调用类型

根据函数调用时所处位置,域(doamin),调用目的不同,GS 支持多种函数的调用方式,其大致类型如下:

调用类型说明
直接调用同一文件或 #include 引入的函数
. 调用调用外部函数或当前域对象的函数
.? 调用安全调用,若函数不存在则不执行也不报错
=> 调用跨域调用,即使域一致也会复制参数和返回值
=>? 调用安全的跨域调用,若对象或函数不存在则不执行
@ 调用异步调用,将函数调用放入协程队列,不阻塞当前执行

4.1 直接函数调用

在同一文件中定义的函数可以直接使用function_name()function(arg0,...)的形式进行调用直接函数调用有两种情况:

  • 外部函数(External function)(Efun)函数调用

  • 在同一文件定义的或使用 #include 进来的函数调用

示例如下:

// 系统函数调用
printlnf("Length of \"Hello wolrd\" is %d", lengthof("Hello world"));
printlnf("Current system info is %O", get_system_info());

// 同一文件函数调用,(其实"#include xxx"是把xxx文件代替"#include xxx"这条语句,
// 实际上也算是同一文件内的函数。
int max(int x, int y)
{
return x > y ? x : y;
}
int x = 1, y = 2;
int ans = max(x, y);
printlnf("Max number of 1 or 2 is %d\n", ans);

示例4-1:直接函数调用示例

自行运行上述程序可以看到直接函数调用成功调用了legthofget_systeminfomax函数并得到了相应地结果。

4.1 .型调用

    .型函数调用一般用于调用GS外部(Efun)函数或调用本域(domain)的某个object的函数,比如:

// 调用GS外部函数
float val = math.abs(-3.14); // 使用math中的abs()函数,求绝对值
printlnf("The absolute value of -3.14 is %O", val);

// 调用本域的object函数
// obj.gs
public void print_hello_world()
{
write("hello world");
}

// test.gs
object obj = this_object();
obj.print_hello_world();

示例4-2:.型函数调用示例

自行运行上述程序可以看到.型函数调用成功调用了math.abs函数,及通过持有当前object实例调用了publicprint_hello_world函数。

部分外部函数可以直接调用,如man system.core展示的所有外部函数,而部分外部函数需要通过点型调用,如man math展示的所有外部函数。具体内容请参考手册文档。

4.2 .? 型调用

 .?型函数调用用法与 .型调用类似,不同的的是?代表的是本调用可空。即调用的函数允许不存在。

  • 当目标调用函数存在时,本调用等效于 .型调用

  • 当目标调用函数不存在时,本调用不会报错而是什么也不做

    该调用设计的主要目的是为了满足使用者在不清楚一个函数存不存在的时候减少判断,方便书写,减少心智负担。举个例子:

// obj.gs
public void print_hello_world()
{
write("hello world");
}

// test.gs
object obj = this_object();
obj.print_hello_world(); // 相当于.型调用
obj.?if_exist_function(); // 不调用也不报错,相当于没有这条语句

示例4-3:.?型函数调用

输出的结果如下:

hello world

    可以看到我们使用.?型调用了一个不存在的函数if_exist_function,但该函数调用没有报错,什么都没有发生。

    除.?型调用以外还有?.型调用以及?.?型调用,其含义如下:

  •  ?.调用:当调用的object不存在时,不会报错,相当于没有该语句。如果存在的话相当于.调用。

    • 一个简单的 ?.调用示例如下:

    • public void hello()
      {
      writeln("Hello world.");
      }
      object ob = this_object();
      ob.hello();

      object ob_invalid = nil;
      ob_invalid?.hello(); // ob_invalid 为无效object,相当于没有调用
  •  ?.?调用:当调用的object不存在或者目标函数不存在时,不会报错,相当于没有该语句。否则相当于.调用。

    • 一个简单的?.?示例如下:

    • public void hello()
      {
      writeln("Hello world.");
      }
      object ob = this_object();
      ob.hello();

      object ob_invalid = nil;
      ob_invalid?.?not_exist_function(); // ob_invalid 为无效object,not_exist_function函数不存在,相当于没有调用

4.3 =>型调用

=>型函数调用表示跨域调用。当对象不在当前域的时候,调用对象的函数需要使用跨域调用。 当对象在当前域的时候,使用跨域调用相当于.型函数调用。关于域的概念在后文关键概念中阐述,现在我们可以将其理解为一把大锁。

注意以下示例为一个双文件示例,需将文件拆分为xxx.gstest.gs并均放在driver的根目录下,并以test.gs 作为启动脚本:

// xxx.gs
int i = 0;

public void test()
{
i = i + 1;
}

public void push_back_one(array arr)
{
arr.push_back(1);
}

// test.gs
handle dom = domain.create("xxx"); // 新建一个域
object obj = load_static("/xxx.gs", dom); // 将obj绑定到新建的域xxx
obj=>test(); // 此时必须使用跨域调用

array arr = [];
obj=>push_back_one(arr);
printlnf("After cross domain call, arr is %O", arr); // 跨域调用传入的参数及返回值会复制。 此时必须使用跨域调用

示例4-4: =>型函数调用示例

从上述示例我们可以看到,参数arr在跨域时发生了复制,由于传入的为复制后的arr调用push_back后原arr内容并未发生改变。即便目标域与当前域一致,依然会导致传入参数和目标函数返回值的复制

4.4 =>? 型调用

用法类似于=>型函数调用,用途类似于.?型函数调用,后面加个?表示这个调用的目标函数可能不存在。示例如下(注意同样为双文件示例):

// xxx.gs
int i = 0;

public void test()
{
i = i + 1;
}

// test.gs
handle dom = domain.create("xxx"); // 新建一个域
object obj = load_static("/xxx.gs", dom); // 将obj绑定到新建的域xxx
obj=>?not_exist(); // 此时必须使用跨域调用,函数不存在,调用相当于不存在

示例4-5: =>?型函数调用示例

可以看到我们使用=>?型调用了一个不存在的函数not_exist,但该函数调用没有报错,什么都没有发生。

4.5 @型调用

@型函数调用是实际是一个我们不着急执行的函数调用,也可以理解为异步调用。 比如说我们要加载一个文件,但我又不着急使用这个文件,那我就使用@型函数调用。

@型函数调用会先把这个函数调用放入一个协程队列中, 等待协程调用执行,此时我们是不知道它什么时候执行完毕的。

示例如下:

public void test()
{
write("AAAA");
}

public void print_hello()
{
write("hello ");
}

@test(); // print_world()放入协程队列,就跳到下一句执行
print_hello(); // 直接执行print_hello()

示例4-6:@型函数调用示例

示例的输出如下:

hello AAAA

可以看到尽管我们先使用了@型调用,但函数实际相应地输出AAAA是在后面输出的。也有可能在 shell启动后@test()函数才被真正调用,此时AAAA输出在Shell>输出后面。此时输出如下:

hello
Welcome driver shell.
GS 1.35.250803 Copyright (C) G-bits
Shell> AAAA

注意直接函数调用,.型调用,=>型调用等都可以带有@前缀,其语法形式如下:

  • 直接函数调用形式如: @direct_call()

  • .型函数调用形式如:@ob_instance.dot_call()

  • =>型函数调用形式如:@ob_instance=>cross_domain()

5 函数的变量作用域

  • ​局部变量 (Local Variable)​​:在函数内部定义的变量,只能在函数内部访问。

  • ​全局变量 (Global Variable)​​:也称为 Object Variable,在函数外部定义的变量,可以被同一文件中的所有函数访问。

示例如下:

string hello = "Hello World";

void print_hello()
{
string local_var = "Hello GS";
writeln(hello);
writeln(local_var);
}

// writeln(local_var); // local_var 为局部变量,函数外部不可访问
void cause_error_func()
{
// string hello = "Hello World"; // 禁止局部变量与全局变量同名
}

示例5-1:函数变量与作用域示例

6 匿名函数

GS中有一种函数叫做匿名函数,匿名函数可以捕获其定义时所处环境的变量,从而形成闭包。匿名函数可以作为参数传递给其他函数,也可以作为函数的返回值,从而支持高阶函数和函数式编程风格。对于只使用一次的函数,使用匿名函数可以避免命名冲突和代码冗余,使代码更简洁。

6.1 匿名函数定义

GS的匿名函数定义基本语法为:

[ capture ]( argument_list )
{
// body_of_delegate_function
};

匿名函数语法定义解释:

  • capture表示捕获的外部变量

    • 决定了使用外部变量时按值捕获还是按引用捕获

    • [&] 用到的任何外部变量都按引用捕获

    • [&x] 变量x按引用捕获,其他变量按值捕获,可以指示多个引用捕获项

    • 如果不需要指示引用捕获,可以省略 []

  • argument_list表示函数的形参列表,规则和普通函数的形参列表一致

    • 与C++不同的是,GS不允许省略空的形参列表:()
  • body_of_delegate_function是函数体

    • 函数体是实现函数的最主要模块
    • 如果函数有返回值则需要在每个函数可能运行结束的最后return一个值  

举个例子:

function test()
{
int i = 0;
i++;
printf("in i = %d\n", i); // 输出 in i = 1
function fun = [&]() // 定义引用捕获匿名函数,返回i
{
i++;
return i;
// printf("i = %d\n", i);
};
fun.call_local();
printf("out i = %d\n", i); // 输出 out i = 2
return fun;
}
function fun = test();
printf("out i = %d\n", fun.call_local()); // 输出 out i = 3
printf("out i = %d\n", fun.call_local()); // 输出 out i = 4

示例6-1:匿名函数示例

示例的输出如下:

in  i = 1
out i = 2
out i = 3
out i = 4

 从示例可以看出匿名函数引用捕获了外部的局部变量i,匿名函数中对引用捕获变量的修改也是对原变量的修改。在示例中可以看到函数test中的fun被调用后外部的局部变量i也发生了改变。同时从示例中也可以看到匿名函数可作为function类型的函数返回值或参数。

6.2 变量捕获方式

捕获方式代表了在匿名函数中是如何使同匿名函数外部的变量的,捕获方式主要分为值捕获与引用捕获两种方式。

  • 值捕获: 捕获变量的副本,闭包内的修改不影响原始变量。
  • 闭包未明确指明引用捕获的变量都是值捕获。

值捕获示例如下:

test.gs
#pragma parallel
#pragma disable_warning(closure_pass_by_val_assigned)
public void test()
{
int n = 1;
array a = [];
array b = [];

printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
printf("\n");
// 定义一个闭包
function fn = () {
// 闭包内的修改不会传递到栈上
n = 3;
a = [ 1, 2, 3 ];
b.push_back(9);

// 使用闭包外的的调用栈上的变量
printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
};
// 调用
fn.call();
printf("\n");
printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
}
test();

示例6-2:值捕获变量示例

输出在此:
n=1
a=[]
b=[]

n=3
a=[1,2,3]
b=[9]

n=1
a=[]
b=[9]

从示例可以看到在值捕获的情况下,对值捕获变量的赋值不会传递到匿名函数外部的变量上。变量na尽管在匿名函数内被赋值,但外部的变量内容却未改变。注意b.push_back(9)看似修改了外部的 b,这是因为数组是引用类型。值捕获的是引用本身的副本(可以理解为指针的副本)。闭包内外的 b是两个不同的“指针”,但它们指向同一个数组。因此,通过其中一个“指针”修改数组内容,另一个“指针”也能看到变化。但如果给 b重新赋值(b = [4,5,6]),则不会影响外部的 b

GS 为避免值捕获变量赋值情况造成的混淆,语法上是禁止在匿名函数内部对值捕获变量赋值的,实例中通过#pragma disable_warning(closure_pass_by_val_assigned)跳过了此限制以展示值捕获的特点。

  • 引用捕获: 变量的引用即变量本身**,闭包内的修改直接影响**原始变量

    • [&]:引用捕获所有变量
    • [&var1, &var2,....]:引用捕获特定的变量
test.gs
#pragma parallel
#pragma disable_warning(closure_pass_by_val_assigned)
public void test()
{
int m = 1;
int n = 1;
array a = [];
array b = [];

printf("m=%d\n", m);
printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
printf("------------------\n");
// 定义一个闭包
function fn = [&b,&n]() {
// 引用捕获b和n
// m和a是值捕获
// 值捕获:闭包内的修改不会传递到栈上
m = 3;
a = [ 1, 2, 3 ];

// 引用捕获:闭包内的修改会传递到栈上
b = [ 4, 5, 6 ];
n = 3;

// 使用闭包外的的调用栈上的变量
printf("m=%d\n", m);
printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
};
// 调用
fn.call();
printf("------------------\n");
printf("m=%d\n", m);
printf("n=%d\n", n);
printf("a=%M\n", a);
printf("b=%M\n", b);
}
test();

示例6-3:引用捕获变量示例

示例的输出如下:

m=1
n=1
a=[]
b=[]
------------------
m=3
n=3
a=[1,2,3]
b=[4,5,6]
------------------
m=1
n=3
a=[]
b=[4,5,6]

这个例子清晰地展示了混合捕获方式下,只有引用捕获的变量 (n, b) 的修改会反映到外部。

7 函数式支持

7.1 高阶函数

一般来说,一个函数可以接受另一个函数作为参数,这种函数就称为高阶函数。我们经常用到的一些数组方法,如map、reduce、filter都属于高阶函数的范畴,灵活运用这函数式编程的三板斧可以少写很多代码。  这些函数除了传入数组本身以外,还传入一个函数指针用于对数组中每个元素进行处理。

7.2 map

举个例子,比如我们有个函数f(x) = x ^ 2, 现在想把这个函数作用到数组[1, 2, 3]上,就可以用map()来实现

import gs.lang.array;
array arr = [1, 2, 3];
array arr_new = arr.map((int x){
return x * x;
});
write("arr_new = ", arr_new, "\n");

示例7-1:map 高阶函数示例

输出的结果如下:

arr_new = [ /* sizeof() == 3 */
1,
4,
9,
]

从结果可以看到array.map函数使用传入的匿名函数参数处理每一个数组成员后构成了一个新的数组。不需要map()函数,写一个循环也可以计算出结果,但是那样需要写更多代码进行处理,且并不能一眼明了把f(x)作用在array的每一个元素上并将结果生成一个新的array的含义。

高阶函数实际上将运算规则抽象了,以便于我们清晰地表达复杂的函数。

7.3 reduce

再看看 reduce 的用法,reduce 提供了reduce_leftreduce_right两种方法。

reduce_left为例,reduce_left传入的函数必须接受两个参数,reduce_left会将本次计算的结果和array的下一个元素再作为函数的两个参数。对一个数组arr来说

array arr = [x1, x2, x3]

  • reduce_left的效果就是

    • arr.reduce_left(func) = func(func(x1, x2), x3)
  • reduce_right的效果则是

    • arr.reduce_right(func) = func(x1, func(x2, x3))

举一个最简单的求和的例子

import gs.lang.array;
array arr = [1, 2, 3];
int ans = arr.reduce_left((int x, int y){
return x + y;
});
write("ans = ", ans, "\n");

示例7-2:reduce_left 示例

 输出结果如下:

ans = 6

7.4 filter

 filter就是顾名思义的过滤器了,它传入的函数与map()一样也是依次作用于数组中的每一个元素的,根据返回值是ture或者false来决定保留还是丢弃该元素

举个例子,比如我想过滤得到数组中所有奇数:

import gs.lang.array;
array arr = [1, 2, 3];
array arr_new = arr.filter((int x){
return x % 2;
});
writeln("arr_new = ", arr_new);

示例7-3:filter 高阶函数示例

输出结果如下:

arr_new = [ /* sizeof() == 2 */
1,
3,
]

可以看到filter过滤器为所有数组成员调用输入的匿名函数,当数组成员为偶数时匿名函数返回值为false相应地成员在新建的数组中被丢弃。

7.5 unpack

unpack是 GS 语言中一个非常重要的实用函数,主要用于参数拆包操作。它能够将数组或函数参数列表中的元素拆分成独立的参数,极大提高了函数调用的灵活性。其函数原型为mixed unpack(array|int v)

unpack函数主要的功能分为两个,数组拆包函数参数列表拆包

unpack参数为数组时,unpack将数组中的每个元素拆分为独立的函数参数,示例如下:

array params = [1, "hello", 3.14, true];

// 传统方式(繁琐)
writeln(params[0], params[1], params[2], params[3]);

// 使用 unpack(简洁)
writeln(unpack(params));

示例7-4:unpack数组示例

示例的输出结果如下:

1hello3.14true
1hello3.14true

unpack参数为整型n时,unpack将从所在函数的第n个参数开始的后续所有参数传入待调用的函数的参数。示例如下:

void test(int arg0, string arg1, float arg2, bool arg3)
{
// 等效于 writeln(arg0, arg1, arg2, arg3);
writeln(unpack(0));

// 等效于 writeln(arg1, arg2, arg3);
writeln(unpack(1));
}
array params = [1, "hello", 3.14, true];
test(unpack(params));

示例7-5:unpack整型示例

输出结果如下:

1hello3.14true
hello3.14true

8 陷阱与调试

7.1 跨域调用性能问题

在之前讲过=>型跨域函数调用每次都会拷贝传入的引用类型参数,这是有额外的性能消耗的。示例如下:

array arr = [];
public void test(array arg)
{
return;
}

object this_ob = this_object();
int start_time = time.time_ms();
for(int i = 0 upto 1000000)
{
this_ob.test(arr);
}
printlnf("Object call cost: %dms", time.time_ms() - start_time);

start_time = time.time_ms();
for(int i = 0 upto 1000000)
{
this_ob=>test(arr);
}
printlnf("Cross domain object call cost: %dms", time.time_ms() - start_time);

示例7-6:跨域调用性能问题示例

示例的输出如下:

Object call cost: 28ms
Cross domain object call cost: 89ms

可以看到多次的数组拷贝花费了大量时间,导致跨域调用耗时是非跨域调用耗时的3倍以上。所以应合理设计,避免不必要的跨域调用。

7.2 函数调试示例

接下来让我们使用gslang调试一个简单的函数调用示例,以进一步了解函数调用过程中的参数传递、返回值、变量作用域等内容。示例如下:

// === 全局变量 ===
int global_counter = 0; // 全局变量,所有函数都可以访问

// === 函数定义 ===
public int calculate_sum(int a, int b = 5) // 参数b有默认值
{
// 步骤4: 进入calculate_sum函数
// 参数: a = 3, b = 5 (使用默认值)
int local_sum = a + b; // 局部变量,只在函数内有效
global_counter++; // 修改全局变量

// 步骤5: 准备返回结果
return local_sum; // 返回计算结果
}

public void process_data(int base_value)
{
// 步骤2: 进入process_data函数
// 参数: base_value = 10
int result1 = calculate_sum(base_value);
// 步骤3: 调用calculate_sum(10),等待其返回
// 步骤6: calculate_sum返回15,赋值给result1

int result2 = calculate_sum(base_value, 3);
// 步骤7: 调用calculate_sum(10, 3),传入第二个参数覆盖默认值
// 步骤8: calculate_sum返回13,赋值给result2

// 步骤9: 打印结果
printlnf("reuslt1: %d, result2: %d", result1, result2);
printlnf("global counter: %d", global_counter);
}

public void run()
{
// 步骤1: 程序开始执行
int input_value = 10;

// 调用process_data函数
process_data(input_value);
// 步骤10: process_data执行完毕,返回main函数

// 步骤11: 尝试访问局部变量(会编译错误)
// printlnf("尝试访问局部变量: %d", local_sum); // 错误: local_sum未定义

// 步骤12: 验证全局变量已被修改
printlnf("final global counter: %d", global_counter);
}

// 执行函数调用
run();

调试过程见下动图:

示例7-7:函数调试示例

从示例代码及动图可以看到,按下F5执行程序后,gslang成功命中了函数外的断点。按下F11后相应函数被执行且调试器进入被调用的函数内部,调试器中局部变量列表及局部变量内容也会更新。(部分被优化掉的变量会带有(optimized)后缀)。在函数调用完成后调试器会回退到上级函数的对应位置,并更新返回值变量内容

9 拓展阅读

更多函数相关内容,如函数的参数域,函数相关的外部函数等内容,请参阅以下手册文档链接:

10 总结

通过本章学习,您已经学会了如何定义函数(包括前缀、返回值、参数列表),掌握了默认参数、变长参数等高级用法,并理解了直接调用、跨域调用(=>)和异步调用(@)等多种调用方式的区别与用途。同时,匿名函数和mapfilter等高阶函数为您开启了函数式编程的大门,让代码更简洁灵活。

接下来让我们开始一些特殊的程序结构的学习,如try-catchdefer等语句的内容学习。