跳到主要内容
版本:master

类 (class_map)

1.概述

class_map 是 GS 语言中实现轻量化面向对象编程的核心类型,提供了类似 C++/Java 的类机制。本章面向已经掌握 GS 基础语法、了解 map 类型使用,并希望提升代码组织能力开发者。通过本章学习,您将掌握 class_map 的定义、继承、成员变量/函数、特殊函数等核心概念,能够使用面向对象思想使用 class_map 类型编写更轻量化的 GS 程序。

2. class_map 基础

2.1 class_map 概念

class_map 是在 map 类型基础上构建的类系统。从本质上讲,class_map 是 map 的一个特殊子类型(sub_type 为 1),其设计的根本目的是为了解决对象(Object)的实现不够轻量化的问题。class_map 类型的实现类似 C++/java 的类,且只支持单继承模式。以字段明确,易于维护。与类型明确,便于优化实现。

注意

  • 我们目前的模式是所有数据集合都放在map中,无论简单的,复杂的,易变的还是不变的,这样的好处是很灵活,我们增加key,减少key,代码都不需要变动,只要生成的地方和使用的地方约定好即可

  • 灵活的同时也带来了坏处,时间一久,内容一多,大家都不清楚某个函数传递的map中都有些什么数据,某个对象身上的map中都有些什么数据,只能在runtime的时候去打印,或者来回看代码,很容易出现错漏。

  • 很多情况下,一些对象身上的map和函数传递时的map都是比较固定的,如果能有类似struct的数据结构来保存和传递会更易维护,如果IDE支持得好,补全提示也比较好做

  • 后续JIT优化的时候,类型都是明确的,不需要预测,会比较好做优化。

  • 在使用class_map时,请尽可能保证类型明确。这样编译器可以在编译时尽可能做好类型的检查和约束。

2.2 class_map 声明

class_map 在声明时以class关键字开头的大括号{}内声明与定义class_map 的成员变量及成员函数,示例如下:

class A 
{
int a = 0;
public int foo(A self)
{
return self.a;
}
}

实例2-1:class_map 声明示例

2.3 class_map 成员

2.3.1 成员变量

成员变量是在类中定义的变量,成员变量储存在类实例之中(在 1.32.250427 以及之前的版本,成员变量被分成两部分储存(即实例 + 元表)。之后的版本,成员变量将直接储存在实例之中)。

  • 子类可以覆盖父类的同名 public 和 protected 成员变量(这么做的意义仅仅是提供不同的初始值)。
  • 成员变量的默认访问权限是 public,除非在成员变量前面加上 private 或 protected 关键字。
  • 编译器会为成员变量的索引操作进行类型检查,如果被访问成员的实例有明确 类型,在编译期能够为索引表达式推导其成员类型。如果类型没有对应的成员定义,能在编译期及时给出诊断。
  • 如果被索引的实例没能明确类型,编译期检查无法执行,运行时如果索引的成员不存在,则结果为 nil(和 map 的 行为类似)。

示例如下:

class Example
{
int a = 1; // 成员变量,默认访问权限为 public
private string b = "Hello"; // 成员变量,访问权限为 private
protected float c = 3.14; // 成员变量,访问权限为 protected
}

Example ex = Example.new();
ex.a = 2; // 设置成员变量 a 的值为 2
// ex.b = "World"; // 错误,因为 b 是 private 成员变量,不能在类外访问
// ex.c = 3.14; // 错误,因为 c 是 protected 成员变量,不能在类外访问
// ex.non_existent = 42; // 错误,如果 Example 类没有定义 non_existent 成员变量,编译期会报错

mixed m = ex; // 尝试修改为 Example m = ex; // 修改后编译不通过
writeln(m.non_existent); // 错误,但是此处 m 没有类型信息,因此编译期OK,运行时得到 nil

示例2-2:class_map 成员变量实例

可以自行放开相应注释,运行代码查看相应报错。示例可以看到protectedprivate限定词对于class_map成员变量访问权限的控制。以及编译器对应明确的class_map 类型的索引操作的编译时检查。

2.3.2 成员函数

成员函数是在类中定义的函数,成员函数储存在元表中(在 1.32.250427 以及之前的版本中,可以像访问成员一样访问到成员函数;之后的版本中,只能从元表中取出函数,或者在方法调用和创建方法闭包中访问成员函数)

class Example
{
void foo(Example self)
{
printf("Hello from Example.foo\n");
}
}
Example ex = Example.new();
ex.foo(); // 调用成员函数, OK

// function f1 = ex.foo; // 1.32.250427 及之前的版本:OK;
// 之后的版本,foo被视为成员变量,找不到foo成员变量;编译期报错或在运行时得到 nil
function f2 = (: ex.foo :); // OK
function f3 = Example.foo; // OK
function f4 = ex.get_class().foo; // OK,等效于 Example.foo

示例2-3:成员函数示例

  • 普通的成员函数始终需要定义名为 self 的首参数,首参数的类型应该是类的类型。

  • 使用 static 修饰的成员函数被称为 静态成员函数,静态成员函数不要求声明 self 参数,并且可以通过元表直接调用。

  • 需要注意的是,运行时是根据方法调用的 . 符号前的对象是 实例元表 来决定是否传入自身作为首个参数的。因此尝试使用实例调用静态成员函数时,仍然需要传入自身作为首个参数。

2.3 class_map 构造与析构

2.3.1 构造函数(__new)

  • class_map 通过调用class_map.newclass_map.new_by_map 函数创建实例。

  • 在 class 内定义名为 __new 的成员函数会被视为构造函数,构造函数应该是一个非静态的成员函数,可以从 class_map.newclass_map.new_by_map 中得到额外的构造参数。

  • 构造函数只有在手动调用和 class_map.new / class_map.new_by_map 创建实例时才会被调用。

  • 创建实例时,如果实例本身有定义构造函数,则不会自动调用父类的构造函数,若需要调用父类的构造函数请通过Base.__new(...)手动调用。

class Base
{
public void __new(Base self)
{
printf("Base constructor called\n");
}
}
class Derived : Base
{
public void __new(Derived self, string msg)
{
Base.__new(self); // 手动调用父类的构造函数
printf("Derived constructor called with: %s\n", msg);
}
}
Derived instance = Derived.new("Helloworld"); // 构造参数被传递给 Derived 的构造函数

示例2-2:class_map 构造函数

示例输出结果如下:

Base constructor called
Derived constructor called with: Helloworld

从示例输出结果可以看到,通过 Derived.new创建一个新的Derived实例时,类内部的__new构造函数被调用。Derived.new函数的第一个参数"Helloworld"对应__new构造函数中的第二个参数msg。以及通过Base.__new函数我们成功调用了父类的构造函数。

2.3.2析构函数(__destruct)

  • 析构函数是在类中定义名为 __destruct 的成员函数,析构函数会在实例被GC机制释放,或者手动调用 class_map.close() 调用。

  • 析构函数应该是一个非静态的成员函数,析构函数应该只有 self 参数。

  • 只有使用 class_map.newclass_map.new_by_map 创建的实例才会触发析构函数。

class Example
{
public void __destruct(Example self)
{
printf("Example instance is being destroyed\n");
}
}
Example ex = Example.new();
Example ex2 = ex.dup(); // 复制实例

ex.close(); // 手动调用析构函数,输出:"Example instance is being destroyed\n"
ex2.close(); // ex2 不是被 new/new_by_map 创建的实例,因此不会触发析构函数

示例2-3:class_map 析构函数

示例的输出结果如下:

Example instance is being destroyed

可以看到我们通过Example.new()创建的 class_map 实例ex调用手动调用close函数后,其被 GC 回收,相应的__destruct函数被调用。并输出了Example instance is being destroyed。而对于通过dup赋值创建的ex2实例,由于其不是通过new/new_by_map创建的,析构函数__destruct无法被调用。

2.5 class_map 序列化

2.5.1 默认的序列化和反序列化

如果没有自定义的序列化和反序列化函数,jsonmsgpack 会按照默认规则,将 class_map 的所有键值对加上其类型信息一起序列化;也能按照默认的规则,将序列化结果反序列化产生 class 实例:

class Example
{
int a = 1;
string b = "Hello";
}
Example ex = Example.new();

string dat = json.save(ex); // 序列化为 JSON 字符串
writeln(dat); // 输出: {"a":1,"b":"Hello","__class__":"Example","__program__":"/test.gs"}
json.parse(dat); // 得到新的 Example 实例:Example{ "a" : 1, "b" : "Hello" }

示例2-4:默认的序列化与反序列化

关于自定义的序列化(__serialize) 和反序列化(__deserialize)函数的使用,请参考下面的章节。

2.5.2 序列化函数(__serialize)

在类中定义名为 __serialize 的成员函数会被视为序列化函数,序列化函数应该是一个非静态成员函数, 返回值类型可以是任何可被序列化的类型,仅接收 self 参数。在 json 和 msgpack 对class实例执行序列化时,会调用此函数,序列化函数的返回值将代替class实例被序列化。

class Example
{
int a = 1;
public mixed __serialize(Example self)
{
return {"a" : self.a * 2}; // 序列化时将 a 的值乘以 2
}
}
Example ex = Example.new();
writeln(json.save(ex)); // 输出 {"a": 2}

示例2-5:class_map 序列化函数示例

如果class 有定义反序列化函数 __deserialize,则序列化结果将被包装成一个带有class元数据(类名,脚本名或序列化ID)的map,这将被用于之后反序列化:

class Example
{
int a = 1;
public mixed __serialize(Example self)
{
return {"a" : self.a * 2}; // 序列化时将 a 的值乘以 2
}
public static Example __deserialize(map data)
{
Example ex = Example.new();
ex.a = data["a"] / 2; // 反序列化时将 a 的值除以 2
return ex;
}
}
Example ex = Example.new();
writeln(json.save(ex));
// 输出: {"__class__":"Example","__program__":"/test.gs","__value__":{"a":2}}

示例2-6:带有反序列化函数的 class_map 的序列化示例

2.5.3 反序列化函数(__deserialize)

在类中定义名为 __deserialize 的成员函数会被视为反序列化函数,反序列化函数是一个静态成员函数,接收一个参数,这个参数用于接收从序列化数据中读取出的原始类型,并通过反序列化函数将其转化成最终类型。反序列化函数的返回值类型通常应该是类的类型。

class Example
{
int a = 1;
public static Example __deserialize(map data)
{
Example ex = Example.new();
ex.a = data["a"]; // 从序列化数据中读取 a 的值
return ex;
}
}
Example ex = Example.new();
string dat = json.save(ex);

Example ex2 = json.load(dat); // 反序列化时调用 Example.__deserialize
writeln(ex2);
// 输出:
// class Example{ /* sizeof() == 1 */
// "a" : 1,
// }

示例2-7:反序列化函数

2.6 class_map 继承

class_map类可以通过 : 符号来继承其他类。被继承的类被称为 父类,继承的类被称为 子类。子类可以覆盖父类公开public或保护protected的成员变量和成员函数。示例如下:

class Base
{
public int a = 1; // 成员变量
public void foo(Base self) // 成员函数
{
printf("Base foo: %d\n", self.a);
}
}

class Derived : Base // 继承 Base 类
{
public int b = 2; // 新增成员变量
public void foo(Derived self) // 重写成员函数
{
printf("Derived foo: %d, %d\n", self.a, self.b);
}
}

Base base_ = Base.new();
base_.foo(); // 调用 Base 类的 foo 方法,输出 "Base foo: 1"
Derived derived = Derived.new();
derived.foo(); // 调用 Derived 类的 foo 方法,输出 "Derived foo: 1, 2"

示例:2-8 继承示例

3 底层实现

class_map是map的一个子类型,底层实现为sub_type为1的map类型,所有的class_map都有同一个sub_type。这样实现的主要原因是我们对于子类型的判断比较严格,而对于class的类型判断,确实需要考虑继承关系的,无法简单得用底层sub_type的方式来处理。整体的实现上,class_map 就是常量 map 的语法糖,比如这样的一段代码:

class A 
{
int a = 1;
};

class B : B
{
string b = "bb";
};

基本等价于这一段代码:

const map A = {
"__class__" : "A",
"a" : 1,
};

const map B = A + {
"__class__" : "B",
"__inherits__" : ["A"],
"b" : "bb",
};

示例3-1:class_map 等价实现示例

当然并不完全等价,我们还在编译时保存了类的全局信息,类名对应哪个常量map,类中有哪些字段,分别是什么类型等等,更多细节可以看cmm_map_class.h/cmm_map_class.cpp的实现。带成员函数的情况也则是使用了新的常量函数的实现,如这样的一个类:

test_class_map2.gs
class A
{
int a = 2;
public void __new(A self, int a)
{
self.a = a;
};
public void foo(A self, int a)
{
printf("foo = %d\n", a + self.a);
};
}
printf("A = %O\n", A);
A a = A.new(3);
printf("A.new(3) = %O\n", a);
printf("program functions = %O\n", this.get_program().get_functions());
a.foo(10);

示例3-2:成员函数实现示例

打印出来会看到:

A = { /* sizeof() == 5 */
"a" : 2,
"__new" : (: .__new@A :)<no this>,
"foo" : (: .foo@A :)<no this>,
"__class__" : "A",
"__inherits__" : [ ],
}
A.new(3) = class A{ /* sizeof() == 1 */
"a" : 3,
}
program functions = [ /* sizeof() == 3 */
"::entry",
"__new@A",
"foo@A",
]
foo = 13
  • (: .foo@A :)就是这个对象上的一个函数,no_this说明没有绑定对象实例

  • 因为我们要把这个函数放在一个常量map中,常量在编译时生成,肯定还不能有对应的对象示例,也不应该被绑定在某个域,为了支持常量函数,我们支持了让某个函数不绑定具体的对象实例,而是可以在调用的时候指定,可以看下remove_ob_instance和call_by_instance的说明

  • 看a.foo(10),实例调用函数的时候,会把自己作为第一个参数传入

  • 打印new出来的class_map实例时,会显示class的members,方便调试

为什么是foo@A,而不是foo?因为我们可以在一个文件内定义多个类,每个类都可以有个foo方法,添加类型后缀用于避免混淆函数名称。比如运行这段代码:

class A { public void foo() {} };
class B { public void foo() {} };
class C { public void foo() {} };
printf("program functions = %O\n",
this.get_program().get_functions());

就能看到,结果是有三个foo函数:

program functions = [ /* sizeof() == 4 */
"::entry",
"foo@A",
"foo@B",
"foo@C",
]

4. 拓展阅读

更多相关内容可以参照以下文档:

class_map手册

class_map 外部函数

5. 总结

通过本章的学习,我们系统地掌握了 GS 语言中 class_map这一轻量化面向对象编程工具的核心概念与使用方法。

核心机制:我们了解到 class_map本质上是 map 的一个特殊子类型,通过编译时的语法糖机制,将类定义转换为包含元数据(如 __class__, __inherits__)的常量 map。其成员变量直接存储在实例中,而成员函数则以其名称与类名组合(如 foo@A)的形式存储在元表中,从而实现了类型的封装和方法的多态。

关键特性:本章重点阐述了以下关键知识点:

  1. 类的声明与成员定义:掌握了使用 class关键字定义类,并声明具有不同访问权限(public、private、protected)的成员变量和成员函数。
  2. 实例生命周期管理:理解了通过 __new构造函数进行对象初始化,以及通过 __destruct析构函数和 close()方法进行资源清理的机制。
  3. 序列化控制:学会了利用默认的序列化行为,以及通过自定义 __serialize__deserialize函数来精确控制对象的序列化与反序列化过程。
  4. 继承与多态:理解了 GS 支持的单继承模型,以及子类如何覆盖父类的成员函数。

设计初衷与价值class_map的设计解决了实例(Object)实现不够轻量化的问题以及纯 map 数据结构字段不明确、维护困难的问题。它通过引入编译时类型检查,显著提升了代码的可靠性和可维护性。总而言之,class_map是 GS 语言中实现轻量化结构化和可维护代码的重要工具。