mcfpp面向对象——类的创建和销毁

||

在之前的文章中,我们提到过了在mcfunction中实现面向对象编程的基本思路,主要说明了类这种数据的储存方式。现在,我们来说一说怎么创建一个类,以及回收一个类占用的资源。老规矩,在写这篇文章之前,我的思路仍然是不清晰的,是边写边想的。

在这之前呢,我们还是先放上我们的一个基本的类:

class Student extends People{
    public static int id = 0;

    public int score;

    public int temp = 1 + 1;

    public Student(int score,entity target){
        super(target);
        id++;
        this.score = score;
    }

    public static getid(static int id){
        return id;
    }

    public getscore(static int score){
        return score;
    }
}

class People{
     entity target;

     public People(entity target){
         this.target = target;
     }
}

复习:数据的储存方式

在我们开始之前,我们还是先复习一下这个类的实现过程,以及我们是怎么储存这个类里面的数据的。

这是一个相当简单的类,我们先不考虑继承的问题,只看Student类中的数据。那么,我们可以看到这个类中首先有一个静态的int数据id和一个成员int数据scoreid作为一个简单的计数器,每当创建一个新的对象的时候就增加1,而score则是这个学生的分数,它是在构造函数的调用过程中发生的。里面还有一个不明所以的temp变量,它没有实际意义,只是作为一个在类成员变量声明的时候即初始化的例子存在。

根据我们上一篇文章的说明,我们将使用一个标签为test_class_Student_pointer的实体作为我们的地址指针,它将在记分板test_class_Student_index上拥有数值0作为地址。它同时还会在记分板test_class_Student_score上拥有一个分数作为它的成员score的值。除此之外,我们还会有一个名为test_class_Student_static的实体作为我们静态成员储存的地方。它将在test_class_Student_static_id上拥有一个分数,对应静态成员id的值。

这是一个相当简单的类,只包含记分板,不过凡是都是从简单的例子说起嘛。

类的解析

类的解析是编译器应该做的事情。编译器需要找到这个类有那些成员,那些方法,并将它们注册到缓存中。

现在我们只考虑成员和构造函数,把上面的类再稍微简化一下。

class Student{
    public static int id = 0;

    public int score;

    public int temp = 1 + 1;

    public Student(int score){
        id++;
        this.score = score;
    }

}

嗯,现在看起来清楚多了呢!

编译将会从上往下遍历,发现三个成员变量,一个是静态的,一个还有初始化表达式。我们的编译器会把这些都存在缓存里面。按照目前已有的实现,我们的编译器会把静态成员和非静态的成员放在不同的缓存里面,

这个时候编译器只是读取到了这个类有那些数据,并没有创建一个类,因此其中的表达式相关的上下文只是被缓存起来了,并没有进行计算。在创建实例或者初始化类的时候,将会计算对应的表达式。

类的创建

类的创建分为类的初始化和类的实例化。在类的初始化的过程中,储存类的静态成员相关数据的实体将会被生成(即前文中的带有标签test_class_Student_static的实体),同时对应的静态成员将会被初始化(如果它们有初始化的计算表达式的话)。

在这里,我们的静态成员是id,它将会被初始化为0,因此我们将会以实体test_class_Student_static为上下文执行函数student/static_load.mcfunction

#
execute as @e[tag=test_class_Student_static] run function test:student/static_load

#初始化
scoreboard players set @s test_class_Student_static_id 0
#初始化地址索引
scoreboard players set $index test_class_Student_index 0

然后是类的实例化。类的实例化是发生在函数中的(即使是类成员初始化的时候也是在一个匿名的init函数中进行的)。例如,我们有

//...
Student stu = new Student(100);
//...

当我们的编译器发现需要创建一个对象的时候,它会首先创建一个指针实体:

#不知道在哪一个函数,反正这个函数里面new了一下呢
summon marker ~ ~ ~ {Tags:[test_class_Student_pointer,mcfpp_test_Student_stu]}
#省略了函数进栈出栈和参数传递的过程
execute as @e[tag=test_class_Student_pointer,limit=1] at @s run function test:student/init

注意由于我们的命令本身需要是运行时静态的(动态命令的占用开销可能会很大,而且会让整个命令代码文件逻辑结构变得非常杂乱),因此我们的init函数应当是可复用的:

#test:student/init
#id++
scoreboard players add test_class_Student_static_id 1
#this.score = score;
scoreboard players operation @s test_class_Student_score = test_int_temp_score mcf_temp
#地址赋予
scoreboard players operation @s test_class_Student_index = $index test_class_Student_index
scoreboard players add test_class_Student_index 1

这样我们就完成了一次new的过程啦。我们创建了一个类,赋予了它地址,并给成员进行了赋值。当然,这只是记分板的变量声明,下面是一些更复杂的例子

实体引用的补充

在之前的文章中我们是用这样的方式来引用一个实体的。

最后稍微麻烦一些的就是实体类型的访问了。当一个(一些)实体应当是某一个类的字段时,我们需要给它(们)加上一个标签,标签名不妨设置为xxx_class_classname_zzz(zzz是变量名)。而这些实体都应当在名为记分板xxx_class_classname_entity上有一个分数。如果实体分数和指针分数一样,就相当于实体的地址就是指针指向的地址,那么就可以借此访问到我们类中的实体了。

显然,当我们的一个指针指向了多个实体的时候,这样的方法是能够满足我们的需求的。然而,如果我们一个实体被多个指针所指向呢?记分板索引的方式显然只适用于一个指针指向多个实体的需求,如果多个指针指向了同一个实体,那么显然后面指针的地址就会覆盖前一个指针在实体上标记的地址。

经过激烈的讨论以后,我们最后决定的目标方案是——

把实体类型砍成只能指向单个实体。这样我们只需要对实体进行编号,然后实体类型的变量实际上只是一个记分板数值即可。

然后我们可以注意到,几乎所有命令中对实体的操控都是通过目标选择器完成的,因此我们可以大胆地把目标选择器作为主要的命令执行参数或者命令执行的上下文。如果要获取到实体,则通过selector.select()这样的方法来选择实体。当然,获得的实体通常一个列表的形式储存的,但是由于编译器是运行时静态的,编译器可以发现这个选择器是否只选择了一个实体,因此也可能返回一个单个的实体变量。是否把这两个方法拆开,以及如何拆开还有待商讨。

由于实体指针也是类,类的实例类型本质是一个实体类型这一特点并没有改变,因此我们也可以用和上面实体类型的处理相同的方法来处理类的类型。

#new Test(int score, entity target, nbt data)
#test:student/init
#id++
scoreboard players add test_class_Student_static_id 1
#记分板
#this.score = score;
scoreboard players operation @s test_class_Student_score = test_int_temp_score mcf_temp
#this.target = target;
#实体
#this.target = target;
scoreboard players operation @s test_class_Student_target = test_entity_target mcf_var
#NBT
#this.data = data;
data modify entity @s data.classData.data from storage mcfpp_temp:data data.data
#地址赋予
scoreboard players operation @s test_class_Student_index = $index test_class_Student_index
scoreboard players add test_class_Student_index 1

类的销毁

在新建一个类的时候,我们会生成一个marker实体,而如果marker实体没有被及时清除,就会带来大量的占用。因此我们需要一个东西,它能自动清除我们不需要的类,即垃圾回收器。

垃圾回收器在很多语言中都存在,例如Java,C#。它们的目的都大致相同,即回收不需要的类所占用的资源。

我们首先需要知道什么时候我们的对象需要被回收。显然,当一个对象不再被任何指针引用的时候,它就应该被清除掉了。但是我们应该如何知道一个对象是否在被指针所引用呢?

我们可以在marker中用一个nbt列表来储存指向了这个对象的指针有那些。注意,事实上我们甚至不需要知道是那些指针指向了它,我们只需要关注是否有指针指向了它,因此,我们只需要这样做:

  • 第一,在对象被新建的时候,有两种情况,要么是在表达式中作为算式的一部分,例如new C().qwq(),要么是用在赋值表达式中,例如a = new B()。在前者中,对象使用完毕后,并没有一个变量指向它,因此应当被垃圾回收器回收,而后者则被一个变量所引用。
  • 在marker中将会有一个列表,储存指向了这个实例的指针。但是由于我们处理垃圾时不需要关心哪些实例被那些指针指向了,因此我们只需要关心这个列表的长度而不用关心列表的内容。当一个实例被一个指针指向的时候,我们在这个实例的maker的指针列表中随便添加一个元素;而当一个指针指向一个实例的时候,我们就会在指针原来的实例的marker的指针列表中减少一个元素。
  • 在每个作用域的结尾,我们把所有此栈中指针指向的实例中的列表都减少一个元素。
  • 最后,每个tick的末尾,我们检查所有的实例,观察它们的指针列表是否为0。如果是零,则清除。

这样就是一个可以自动运行的垃圾处理器了。但是我们还需要注意一点,那就是有时候我们在创建一个类以后希望它能一直存在。就例如,我们创建了一种新的实体类型,它以盔甲架为原型,拥有一个记分板分数作为额外的数据。通常来说,它会因为作用域的问题在每一个tick的结尾被删除,除非我们创建一个静态列表把它放在里面保持它可以被访问。但是这样会显得很麻烦。

因此,我们引入一种特殊的类,它不会被垃圾处理器回收。如果我们用static修饰这个类,就把它标记为一个静态类,不会被垃圾回收器处理。然后,我们可以通过一个特殊的方法,即Classname.getall()来获取一个选择器,从而对对象进行批量处理。显然,这样的类需要我们自己创建好销毁的过程,否则会导致严重的性能问题。mcfpp提供了finalize()方法进行重写。垃圾回收器会自动调用这个方法,来处理这个类的销毁问题。

类似文章

发表回复