程序、对象、字段和方法
在 GS 中,一个单独的脚本文件被编译之后会产生一个 程序(Program)实例,通过将程序的路径传递给 load_static 或
new_object 方法,可以创建该程序的 对象(Object)实例。
在程序中可以为 对象 定义的方法和成员变量。
程序 (Program)
程序即从脚本文件编译产生的实体,只有成功通过编译的脚本才会产生程序实例。
当尝试使用 compile_program, load_static 或 new_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;
字段可以被标注为 readonly 或 parallel,如果没有标注,则字段默认为 readwrite。
parallel 方法只能访问 parallel 或 readonly 字段,不能访问 readwrite 字段。
只允许从方法中访问当前脚本中定义的字段,不能直接访问组件中或其他对象的字段。但作为特例的是 const 字段;const 字段
可以被从 import 或 component 导入的脚本中获取(但不能从对象实例中获取)。
// 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 声明常量之外,使用 class 和 enum
也会产生对应的同名常量,这些常量亦可被从外部脚本访问。
通常而言,每个对象的字段实例都是独立的。允许定义 静态字段:
静态字段
使用 static 关键字可以定义静 态字段,静态字段必须被标注为 parallel 或 readonly :
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 方法,不能调用 private 或 protected 方法。
object obj = new_object("example");
obj.public_method(); // Ok
// obj.private_method(); // error
脚本中声明方法时,默认的访问权限是 private,可以通过手动指明的方式修改其访问权限。
不允许定义和组件中重名的 public 或 protected 方法(private 方法允许重名)。
可以直接调用(即隐含地传递 this)组件中的 public 或 protected 方法,但不允许调用组件中的 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_static
或 new_object 方法间接调用,任何情况下都不应当被手动调用。
构造函数允许最多拥有一个参数,该参数通过 load_static 或 new_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,虚函数可以被声明 public 或 protected。覆写虚函数时必须使用相同的访问权限。
覆写虚函数必须拥有相同的返回值类型和形参类型。
虚函数应当是逐级覆写的,即只允许覆写组件中的虚函数。为了便利考虑,允许覆写未覆写虚函数的组件的祖组件中的虚函数:
// 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
{
//...
}