类 (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 成员变量实例
可以自行放开相应注释,运行代码查看相应报错。示例可以看到protected与private限定词对于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.new和class_map.new_by_map函数创建实例。 -
在 class 内定义名为
__new的成员函数会被视为构造函数,构造函数应该是一个非静态的成员函数,可 以从class_map.new和class_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.new或class_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无法被调用。