跳到主要内容
版本:master

程序、对象、字段和方法

在 GS 中,一个单独的脚本文件被编译之后会产生一个 程序(Program)实例,通过将程序的路径传递给 load_staticnew_object 方法,可以创建该程序的 对象(Object)实例。

在程序中可以为 对象 定义的方法和成员变量。

程序 (Program)

程序即从脚本文件编译产生的实体,只有成功通过编译的脚本才会产生程序实例。

当尝试使用 compile_program, load_staticnew_object 方法时,如果路径对应的程序实例未被创建,GS 会尝试编译 对应的脚本。默认情况下,GS 不会检查文件是否被改变,也不会自动重新编译;如果需要重新编译一个脚本,请参考热更新功能。

对象 (Object)

对象是程序的实例化产物,可以通过 new_object 方法创建。每个对象实例都属于一个 (即便 是 parallel object 也依然如此)。如果尝试调用 object 的非 parallel 方法,GS 需要让协程跨入到目标对象所在的域。

按生命周期划分,对象可以被分为:

  • 静态对象

    静态对象可以通过 load_static 方法创建,GS 的一些运行时机制也会自动创建静态对象。正常情况下,一个程序最多对应一个 静态对象实例。

    静态对象实例除非被手动关闭,否则其自身是一个根对象;因此静态对象的生命周期通常和 GS 的运 行周期相同。

    被手动关闭静态对象后,之后在必要时(调用 load_static 或GS 的运行时机制需要),会创建新的静态对象实例。

  • 一般对象

    通过 new_object 方法创建的对象实例,如果失去所有引用,对象会被回收。

字段 (Field)

在脚本中可以为对象定义成员变量,这些成员变量被称为字段。

int member = 15;
parallel int parallel_member := 20;
readonly int readonly_member := 25;

字段可以被标注为 readonlyparallel,如果没有标注,则字段默认为 readwrite

parallel 方法只能访问 parallelreadonly 字段,不能访问 readwrite 字段。

只允许从方法中访问当前脚本中定义的字段,不能直接访问组件中或其他对象的字段。但作为特例的是 const 字段;const 字段 可以被从 importcomponent 导入的脚本中获取(但不能从对象实例中获取)。

// a.gs
const int const_member = 30; // 可以被外部访问
int member = 15; // 不能被外部访问

// b.gs
import a;
writeln(a.const_member); // Ok
// writeln(a.member); // error

object obj = new_object("a");
// writeln(obj.const_member); // error
// writeln(obj.member); // error

常量字段

常量字段被视为是枚举的补充(因为枚举项的值类型固定,不能是其他类型)。

可以使用 const 关键字定义常量字段,当以这种方式声明常量字段时,字段必须提供初始值, 且初始值应当是一个编译期常量:

const int v = 0; // Ok
// const int w = foo(); // error

除了上述方式定义的常量字段之外,GS中的以下元素也被视为是常量字段:

  • class 定义,产生一个同名的 class 常量(称之为 parent-class)
// a.gs
class Example
{
}

// b.gs
import a;

writeln(a.Example); // Ok
  • 枚举定义,产生一个同名的 map 常量
// a.gs
export enum Enum
{
// ...
}

// b.gs
import a;

writeln(a.Enum); // Ok

除了通过 const 声明常量之外,使用 classenum 也会产生对应的同名常量,这些常量亦可被从外部脚本访问。


通常而言,每个对象的字段实例都是独立的。允许定义 静态字段

静态字段

使用 static 关键字可以定义静态字段,静态字段必须被标注为 parallelreadonly

static parallel array val := make_parallel([]);

静态字段不被储存于任何对象实例(亦不属于静态实例),除非对应的程序被释放,任何时刻访问到的 静态字段都是同一个实例。

在程序的首个对象实例(包括被组件的形式)创建时,在调用其入口方法 ::entry 之前,按照从上到下的顺序初始化静态字段; 静态字段的初始化表达式不应当访问(包括非静态字段在内的)未完成初始化的其他字段。

方法 (Method)

在脚本中可以定义一些函数,这些函数最终都归属某一个对象,可以从对象实例上调用这些函数,这些函数即为对象的方法。

通行规则

object obj = new_object("example");
obj.method();

所有的对象方法默认都隐含一个 this 参数,指向调用该方法的对象实例。

当尝试调用当前对象的方法(即定义在当前脚本或组件中的方法)时,可以省略 this 参数:

void foo()
{
// ...
}
void bar()
{
foo(); // Ok
}

如果需要调用其他对象的方法,必须显式地指定对象实例:

object obj = new_object("example");
obj.method(); // Ok

以显示指明对象实例的方式调用方法时,只允许调用 public 方法,不能调用 privateprotected 方法。

object obj = new_object("example");
obj.public_method(); // Ok
// obj.private_method(); // error

脚本中声明方法时,默认的访问权限是 private,可以通过手动指明的方式修改其访问权限。

不允许定义和组件中重名的 publicprotected 方法(private 方法允许重名)。

可以直接调用(即隐含地传递 this)组件中的 publicprotected 方法,但不允许调用组件中的 private 方法。

// a.gs
public void foo()
{
// ...
}
protected void bar()
{
// ...
}
private void baz()
{
// ...
}

// b.gs
component a;
void test()
{
foo(); // Ok
bar(); // Ok
// this.bar(); // error, 显式地指示对象,不能调用其 protected 方法
// baz(); // error
}

// void foo() {} // error, 不能重名
void baz()
{
// Ok, private 方法允许重名
}

GS 的对象中可以包含一些特殊方法,这些方法遵循一些特殊规则

静态初始化入口 (::static_init)

如果程序定义有静态字段,则在程序中自动为对象生成一个名为 ::static_init 的静态方法。

这个方法在程序的首个对象实例(包括被组件的形式)创建时,在调用其入口方法 ::entry 之前被调用一次,用于初始化静态字段。

入口函数 (::entry)

程序的最外层作用域的所有语句(不包括静态字段的定义)构成程序隐含的入口函数 ::entry

void foo()
{
// ...
}

writeln("Hello, World!"); // 在 ::entry 中

入口函数在构造函数之前被调用,用于初始化各个成员字段。在实例化对象时,按照组合的顺序,调用每个组件的入口函数,然后调用 当前程序的入口函数。

入口函数始终是 private 的,任何情况下都不应当被手动调用。

构造函数 (create)

在所有组件的入口函数执行完毕后,执行程序的构造函数。

// a.gs
void create()
{
writeln("a created");
}

// b.gs
import a;
new_object(a); // 输出 "a created"

构造函数无论被声明为 public, protected 还是 private,都始终是 private 的;构造函数只应当被 load_staticnew_object 方法间接调用,任何情况下都不应当被手动调用。

构造函数允许最多拥有一个参数,该参数通过 load_staticnew_object 方法传递。GS 的运行时机制自动加载的静态对象,会向 构造函数传递 nil 作为参数。

// a.gs
void create(mixed arg)
{
writeln("a created with arg: ", arg);
}
// b.gs
import a;
new_object(a, this_domain(), 123); // 输出 "a created with arg: 123"

析构函数 (destroy)

当对象被 GC 回收,或者被手动调用 close 关闭时,调用对象的析构函数 destroy

void destroy()
{
writeln("object destroyed");
}
object obj = new_object("example");
obj.close(); // 输出 "object destroyed"

析构函数不应当接收任何参数。

当手动调用 close 关闭对象时,会在当前协程调用析构函数;如果被 GC 回收,有专用的协程用于调用析构函数。为了避免阻塞 GC 线程 导致回收效率下降,任何析构函数都不应该执行需要长时间运行或阻塞的操作。

析构函数无论被声明为 public, protected 还是 private,都始终是 private 的;析构函数只应当在被 GC 回收 或 close 时间接调用,任何情况下都不应当被手动调用。

析构函数的调用顺序与构造顺序相反。

虚函数与虚函数的覆写

GS 允许定义虚函数,虚函数是用于实现动态派发的手段。

// a.gs
virtual void foo()
{
writeln("foo from base");
}
public void main()
{
foo();
}
// b.gs
component a;
override void a.foo()
{
writeln("foo from derived");
}

main(); // 输出 "foo from derived"

虚函数的默认访问权限是 public,虚函数可以被声明 publicprotected。覆写虚函数时必须使用相同的访问权限。

覆写虚函数必须拥有相同的返回值类型和形参类型。

虚函数应当是逐级覆写的,即只允许覆写组件中的虚函数。为了便利考虑,允许覆写未覆写虚函数的组件的祖组件中的虚函数:

// a.gs
virtual void foo()
{
//...
}

// b.gs
component a;

// 未覆写 foo

// c.gs
component b;

override void b.foo() // Ok, 覆写了祖组件 a 中的虚函数
{
//...
}

覆写虚函数可以使用 override 关键字或 final 关键字;被以 final 关键字修饰的虚函数不能被再次覆写。

// a.gs
virtual void foo()
{
//...
}
virtual void bar()
{
//...
}

// b.gs
component a;
override void a.foo()
{
//...
}
final void a.bar()
{
//...
}

// c.gs
component b;
override void b.foo() // Ok
{

}
// override void b.bar() // error, 不能覆写 final 方法

不允许虚函数的覆写链出现分叉:

// a.gs
virtual void foo()
{
//...
}

// b.gs
component a;
override void a.foo()
{
//...
}

// c.gs
component a;
override void a.foo()
{
//...
}

// d.gs
component b;
component c; // error, 不能同时继承 b 和 c,因为它们覆写了同一个虚函数

不允许虚函数的覆写链成环:

// a.gs
component b;
override void b.foo()
{
//...
}

// b.gs
component a;
override void a.foo() // error
{
//...
}