跳到主要内容
版本:master

对象(object)

1. 概述

本章将系统介绍 GS 语言中的核心概念——对象(Object)。我们将学习并了解对象的本质、创建方式、生命周期管理以及其与原型(Program)的关系。内容包括如何使用 load_staticnew_object来加载和创建对象,理解对象的初始化过程(::entrycreate())和销毁过程(destruct),并掌握一系列用于操作和查询对象的外部函数。

本章面向已经了解 GS 基本语法,并开始接触面向对象部分的同学或需要回顾对象相关内容的同学。

通过阅读本章,您将能够理解 GS 中对象的定义及其与原型(Program)的区别和联系。掌握创建对象的两种主要方式:加载静态原型 (load_static) 和实例化新对象 (new_object)。了解对象的完整生命周期,包括初始化函数(create)和析构函数(destruct)的调用时机。GS 独特的对象管理机制,即对象必须手动销毁的重要性,以避免内存泄漏。尝试使用常用的对象操作函数来查询信息、管理状态和进行日常开发。

2. object 基础

2.1 object 概念

在GS中,对象(object)是一个包含成员变量和方法的模块。在编写.gs文件时,实际上就是在描述一个对象的原型。对象可以从文件中加载(使用load_static加载原型),也可以通过原型创建新的对象副本(使用new_object)。

与其他面向对象语言不同的是,GS对象的变量无法从外部直接访问,开发者需要在对象中提供接口函数,其他程序通过调用这些函数来操作对象。

特别注意事项:

  • GS对象被设计用于表达较大规模、拥有较长生命周期的数据和方法集合
  • 由于GC不会频繁检测对象,创建后的对象必须手动调用destruct_object方法进行回收
  • 若不释放对象可能导致内存浪费,严重时可能引发内存耗尽崩溃
  • 如果不需要手动释放的对象,或考虑更轻量化的模块结构,可以考虑使用 map 或新版本 GS 的 class_map

2.2 object 创建

2.2.1 object 静态加载

使用load_static可以从文件中加载 object 的原型。使用import import_name;并通过import_name.func_name();调用函数,相应的 object 会在函数调用时静态加载进来。

示例如下(注意多文件实例,需要拆分):

// example.gs
public void hello_world()
{
writeln("Hello world.");
}

// test.gs
import .example;

writeln("Try find example object : ", find_object(example));
example.hello_world(); // 这个操作也将会尝试 load_static example
writeln("Try find example object after function call: ", find_object(example));

object origin_obj_1 = load_static(example, this_domain());
object origin_obj_2 = load_static("./example.gs", this_domain());
writeln("Get static object from string: ", origin_obj_1);
writeln("Get static object from import_name: ", origin_obj_2);

示例2-1:object 创建示例

输出结果如下:

Try find example object : nil
Hello world.
Try find example object after function call: object[2881105:v0]/example.gs<Static>
Get static object from string: object[2881105:v0]/example.gs<Static>
Get static object from import_name: object[2881105:v0]/example.gs<Static>

可以看到example.hello_world()函数调用后example.gsobject被静态加载进来,相应对象实例带有<Static>后缀。此时调用load_static函数,由于example.gsobject已被静态加载,load_static直接返回了已被静态加载的对象实例,即example"./example.gs"指向相同文件时,origin_obj_1origin_obj_2是同一个object。总结如下。

  • 当参数对应的静态对象实例不存在时load_static会编译或查找object对象的program原型,并从program原型创建静态object对象实例。
  • 当参数对应的静态对象实例存在时load_static会直接返回该静态对象实例。
  • import_name.function_name_call()对静态对象的处理与load_static相同。

2.2.2 object 非静态加载

通过调用new_object函数创建非静态加载的对象实例,保持示例2-1的文件结构,修改test.gs的脚本代码为如下所示:

// test.gs 
import .example;
object new_obj_1 = new_object(example, this_domain());
object new_obj_2 = new_object("./example.gs", this_domain());
writeln("object1 created by new object: ", new_obj_1);
writeln("object2 created by new object: ", new_obj_2);

示例2-2:object 的非静态实例创建示例

输出结果如下:

object1 created by new object: object[2877009:v0]/example.gs
object2 created by new object: object[2877012:v0]/example.gs

new_object 会根据传入的参数,编译或查找object对象的program原型,并从program原型创建全新的非静态object对象实例。从示例的输出结果也可以看到每个new_object调用都创建了全新的example.gs非静态对象实例,且看到相应object没有<Static>后缀。

2.3 object 生命周期

对象的创建过程中会按照先后顺序自动的调用脚本中定义的::entry()create()函数进行对象的初始化操作。可以通过手动声明create函数的方式在object创建时执行一些固定的初始化逻辑。

在对象因为GC或手动关闭而销毁时,会调用destruct函数。

2.3.1 入口函数(::entry)

::entry函数(也称为入口函数)是GS语言中一个特殊的、由编译器自动生成的隐式代码块。它代表了.gs文件脚本顶层的执行上下文环境,包含了文件中所有不在任何函数体内的顶层代码。

核心特性

  • 隐式定义:开发者无需(也无法)显式定义void ::entry() {...}函数。
  • 自动生成:编译器会自动将顶层代码组织到::entry函数中。
  • 最先执行:在对象创建过程中,::entry函数是第一个被调用的代码块。
  • 成员初始化:在::entry中定义的变量会成为对象的成员变量,可以在对象的其他函数中访问。
  • 单次执行:每个对象实例的::entry函数在其生命周期内只执行一次。

示例如下:

// example.gs
// ===== ::entry 开始:文件顶层的所有代码 =====
printf("1. This is the first line of '%s'\n", __FUN__); // 在 ::entry 函数内部

string moduleName = "my module"; // 在 ::entry 函数内部
int count = 0; // 在 ::entry 函数内部

// 函数定义及函数定义内部的代码不属于 ::entry
void create()
{
count++; // 修改的是原型中的 count
printf("5. crate function, total: %d, cur function is '%s'\n", count, __FUN__);
}

// 函数定义及函数定义内部的代码不属于 ::entry
void do_something()
{
printf("4. normal function, cur function is '%s' called by '::entry' \n", __FUN__);
}

printf("2. current function is '%s'\n", __FUN__); //在 ::entry 函数内部
printf("3. object state in ::entry function is '%O'\n", this_object().info().state);
do_something(); //在 ::entry 函数内部,调用do_string() 函数
// ===== ::entry 结束 =====
// 在 ::entry 函数调用结束后,声明的 create 函数被调用。

示例2-3:::entry函数概念示例

示例的输出结果如下:

1. This is the first line of '::entry'
2. current function is '::entry'
3. normal function, cur function is 'do_something' called by '::entry'
4. crate function, total: 1, cur function is 'create'

宏定义__FUN__会被展开为代码所在函数的外部函数名称。从示例及示例的输出结果可以看到,在.gs脚本顶层的不在任何函数内的代码实际是都归属于::entry函数的。而函数createdo_something内的代码__FUN__展开后可以看到代码不归属于::entry函数。在调用::entry函数时,对象处于ObjectState.CREATING(2)状态,这意味着如果::entry中发生错误,对象创建就会失败。

::entry函数在对象创建时立即执行,主要用于初始化对象的成员变量、执行对象启动时需要立即运行的逻辑、建立对象的基础状态、调用其他函数进行初步设置。

重要注意事项

  • ::entry中定义的变量会成为对象的成员变量,在整个对象生命周期内存在。
  • ::entry中的代码执行顺序就是其在文件中的书写顺序。
  • 如果::entry中发生错误,对象创建过程会中断,create函数将不会被执行。

2.3.2 构造函数(create)

概念定义

create函数是GS对象中一个可选的、由开发者显式定义的构造函数。它在::entry函数执行完毕后被调用,用于完成对象的特定初始化逻辑。

核心特性

  • 显式定义:需要开发者主动声明和实现。
  • 参数支持:最多可以接收一个构造参数。
  • 灵活可选:不是必须实现的函数,根据需要定义。
  • 执行时机:在::entry执行完成后自动调用。
  • 单次执行:每个对象实例的create函数在其生命周期内只执行一次。

代码示例如下:

example.gs
// example.gs
string config_data = "default";

void create(string msg)
{
// create 函数 *最多* 可以传入 *一个* 构造参数
// 参数在 load_static 或 new_object 时传入
printf("On create: %s. Object is '%O'\n", msg, this_object());
printf("In create function, object state is %O\n", this_object().info().state);
config_data = msg; // 使用构造参数初始化成员变量
}

public string get_config()
{
return config_data;
}
test.gs
// test.gs 
import .example;
// 传入构造函数参数 "init data"
object ob = new_object(example, this_domain(), "init data");
printf("Config data: %s\n", ob.get_config()); // 输出: Config data: init data

示例2-4:构造函数示例

如示例所示,构造函数在::entry函数完全执行完毕后调用,同时只能传入最多一个参数。在create构造函数中对象处于ObjectState.CREATING(2)状态,同样意味着如果create中发生错误,对象创建就会失败。

构造函数主要用于接收并处理构造参数、执行复杂的初始化逻辑、建立对象运行所需的外部依赖、验证对象初始状态的正确、根据参数动态配置对象行为。

参数传递机制

构造参数通过new_objectload_static的第三个参数传递:

// 创建对象并传递构造参数伪代码
object obj1 = new_object(program_name, domain, "construction_parameter");
object obj2 = load_static(program_name, domain, "construction_parameter");

重要注意事项

  • create函数最多只能有一个参数,如果需要传递多个参数,可以使用数组、映射等复合数据类型。
  • 如果定义了带参数的create函数,但在创建对象时没有传递参数,将会触发运行时错误。
  • create函数中发生的异常会中断对象创建过程,对象将会创建失败。
  • 静态加载的对象(load_static)只在第一次加载时执行create函数,后续调用复用已创建的对象。

2.3.3 析构函数(destruct)

概念定义

destruct函数是GS对象中用于清理资源的析构函数。在对象被销毁时自动调用,确保资源得到正确释放。

核心特性

  • 资源清理:专门用于释放对象占用的资源。
  • 自动调用:在对象销毁时由系统自动触发。
  • 单次执行:每个对象实例的destruct函数在其生命周期内只执行一次。
  • 最后机会:是对象生命周期中最后一个被调用的函数。

触发时机

destruct函数在以下情况下会被调用:

  1. 手动销毁:调用destruct_object(obj)函数。
  2. 关闭操作:调用obj.close()方法。
  3. 垃圾回收:当对象不再被引用且被GC检测到时。

一个简单的示例如下:

exmalpe.gs
// example.gs
void destruct()
{
// 做一些资源清理工作
printf("On destruct of '%O'.\n", this_object());
printf("In destruct funtion, handle state is %O\n", this_object().info().handle.state);
}
// test.gs
import .example;
object ob = new_object(example, this_domain(), "new_object()");
printlnf(HIG"destruct_object '%O' is called"NOR, ob);
destruct_object(ob); // 手动触发destruct函数

示例2-5:析构函数示例

如示例所示,析构函数destruct在手动调用destruct_object(object_instance)、调用object_instance.close()、或垃圾回收(GC)机制回收对象时执行,且析构函数仅执行一次。在析构函数中对象处于ObjectState.DESTRUCTING(-1)状态。

析构函数destruct主要用于处理必要的资源释放、各句柄handle的关闭、未持久化数据的保持等工作。

重要注意事项

  • destruct函数中应避免调用可能失败或阻塞的操作。
  • destruct函数执行期间,对象已处于"正在关闭"状态,理论上不应再被其他代码使用。
  • 如果destruct函数中发生异常,异常中断destruct函数,但不会中断销毁过程。
  • 对于持有重要资源的对象,建议显式调用destruct_object而不是依赖GC,以确保资源及时释放。
  • 静态对象(通过load_static加载)在程序运行期间通常不会自动销毁,需要手动管理其生命周期。

2.3.4 完整的生命周期流程

正常生命周期序列

  1. 对象创建new_object/load_static
  2. 入口初始化::entry函数执行
  3. 构造过程create函数执行
  4. 对象使用 → 正常的业务逻辑处理
  5. 析构过程destruct函数执行(手动或GC触发)
  6. 对象销毁 → 资源完全释放

::entrycreatedestruct函数按顺序先后调用的示例如下:

// example.gs
void create(string msg)
{
// create 函数 *最多* 可以传入 *一个* 构造参数
// 参数在 load_static 或 new_object 时传入。
printf("On create: %s. Object is '%O'\n", msg, this_object());
}

void destruct()
{
printf("On destruct of '%O'.\n", this_object());
}

printf("On ::entry of '%O'.\n", this_object());
//test.gs
import .example;
object ob = new_object(example, this_domain(), "new_object()");
printlnf(HIG"destruct_object '%O' is called"NOR, ob);
destruct_object(ob);

示例2-6:对象生命周期示例

示例的输出结果如下:

On ::entry of 'object[2881105:v0]/example.gs'.
On create: new_object(). Object is 'object[2881105:v0]/example.gs'
destruct_object 'object[2881105:v0]/example.gs' is called
On destruct of 'object[2881105:v0]/example.gs<Destructing>'.

​ 从输出结果可以看到,在创建的object的声明周期内特殊函数的调用顺序如下:

  • ::entry函数:object 创建时首先调用::entry函数。
  • create函数:::entry函数调用结束后调用create函数。
  • destruct函数:GC或手动关闭或销毁时调用destruct函数。

2.4 object 函数调用

​ 在之前的程序结构/函数的章节我们已经简单聊过函数的调用类型等内容。通过持有object实例我们可以很方便的进行..?=>=>?型等各种调用,此处不在赘述。一个简单的代码示例如下:

// example.gs
array arr = [];
public void push_one()
{
arr.push_back(1);
}

public int get_arr_len()
{
return arr.length();
}
//test.gs
import .example;
object ob = new_object(example, this_domain(), "new_object()");
printf("Init array len in ob is '%d'\n", ob.get_arr_len());
ob.push_one();
ob.?push_one();
ob=>push_one();
ob=>?push_one();
printf("After 4 push one arr len is '%d'\n", ob.get_arr_len());

示例2-7:object函数调用示例

输出结果如下:

Init array len in ob is '0'
After 4 push one arr len is '4'

从示例代码及其输出可以看到,给出的..?=>=>?型的函数均被成功的调用了,其他调用类型、一些应当调用失败、一些调用相当于没有调用的情况请修改代码自行尝试。

2.5 object 域

在创建object过程中诸如示例2-1或示例2-2中调用的new_objectload_static函数,我们可以看到我们均传入了一个this_domain()的参数,用于指定在当前的域中创建object。在之前的程序结构/函数的章节介绍=>跨域调用时我们简单的将域描述为了一个大锁,现在让我们进一步思考。

假如当多个并行的执行流(线程)同时**持有同一个object句柄,并调用函数操作同一个对象数据时,会发生什么?**比如如下示例:

// bank_account.gs
int balance = 1000;

public void withdraw(int amount)
{
if (balance >= amount) {
// 模拟一些处理时间
coroutine.sleep(10);
balance = balance - amount;
printf("withdraw %d, balance: %d\n", amount, balance);
}
}

public int get_balance() {
return balance;
}

示例2-8:object 域思考示例

​ 现在考虑两个不同的执行流同时调用 ob=>withdraw(800);,**你认为最终余额会是多少?**这个简单的例子让我们触及了GS编程的一个核心概念或问题:GS如何处理或解决并行程序的资源竞争问题?

  • 🤔 这个对象可以被多个执行流同时访问吗?
  • 🤔 对象的方法是否需要考虑并行安全问题?
  • 🤔 如何设计对象才能既保证安全又保持性能?
  • 🤔 GS提供了哪些机制来帮助处理并行竞争?

详细的解答会在下一章节的域(Domain)中给出,现在暂时搁置我们的疑问与好奇,让我们继续Object生命周期学习。

3. 拓展阅读

更多 object 相关内容,object 的外部函数如获取 object 状态,判断 object 是否被销毁,获取 obejct 的组件或域信息。以及更多的 object 具体细节,请参照手册文档,链接如下:

4. 总结

本章详细介绍了 GS 中对象(object)的核心概念与基础操作。我们深入理解了对象的本质——既是包含数据和方法的模块,也是需要通过 load_staticnew_object创建、并必须手动调用 destruct_object销毁的独立实体。我们学习了对象的完整生命周期,从 ::entry初始化、create构造到 destruct析构的整个过程,并掌握了通过不同操作符进行函数调用的方法。

我们从一个关键问题出发,初步探讨了对象的并发访问挑战:当多个执行流同时操作同一对象时,如何保证数据的一致性与线程安全?这个问题让我们触及了 GS 对象模型中一个更深层的核心机制——域(Domain)。在下一章中,我们将正式揭开**域(Domain)**的神秘面纱,让我们继续这场学习之旅。