MCFPP面向对象——多态
多态
在之前的文章中,我大致对MCFPP类的实现进行了解读。在那个时候,我提出了多态的基本的实现方式:
多态
整个面向对象拥有三个基本特征,即封装、继承和多态。而现在,我们要实现的最后一个功能便是多态。多态要求即使是同一个接口(父类),因为实际引用的是使用不同的子类实例而执行不同的方法。前文中我们提到,我们要求子类的指针实体同样也含有它的父类的tag,并且也要在父类的记分板上有分数,这都是为了保证一个父类的变量也能指向它。而在执行方法的时候,我们需要有一个额外的函数。在这个函数中,我们将根据这个指针实体上实际的子类标签来分流。对于多层继承的,应当保证最子的子类(好怪的说法喵)在上面,即假设我们有A、B继承了C,C继承了D,现在我们以一个D类型的指针实体为上下文,那么我们的伪代码应该是:
if(@s.tag.contains(A)) A.func();
else if(@s.tag.contains(B)) B.func();
else if(@s.tag.contains(C)) C.func();
else if(@s.tag.contains(D)) D.func();
而字段由于我们直接覆写,所以直接访问即可。
由于类的实例实体是由Tag来决定的,A类的实体会带有标签A,B类的实体会带有标签B,以此类推。那么如果B继承于A呢,那么A的实体就会同时拥有A,B两个标签,表示这个实体既可以作为A被访问,也可以作为B被访问。在具体访问的过程中,编译器会知道带有B标签的实体可能拥有多态,因此在编译出的代码的具体实现中会再次进行选择判断,看看这个实体有没有A标签。
这种方法不够优雅,而且随着类的继承深度提高,这种方法的消耗会越来越高。现在,我们用最新最热的特性——宏来实现。当然,由于mcfpp的向下兼容性,在没有宏的版本,我们仍然会按照以前的方法进行实现。现在,让我们看看,mcfpp是怎么用宏实现类的多态的。
首先一些基本的宏的使用技巧。由于宏是使用是简单的字符替换,因此我们可以用宏拼装命令的任意部分:
#example:test function example:macro_test {functionID:"example:target"} #example:macro_test $function $(functionID)
在调用example:macro_test
的时候,我们只需要传入适当的参数,就能更改需要调用的函数了。当然,这样包含某些单条的动态命令的函数可以额外制作成一个包,从而方便重复调用,但是那就不是我们今天讨论的内容了。我们的实体中可以包含一个NBT字符串,这个字符串指向了这个实体在调用某个可能会被继承的方法的时候的应该被调用的方法。具体来说,例如我们有这样的mcfpp代码:
class Animal { public void makeSound() { print("The animal makes a sound"); } } class Dog: Animal { public override void makeSound() { print("The dog barks"); } }
Dog
类继承于Animal
类,并重写了其中的makeSound
方法。在编译为mcfunction的时候,如果我们实例化了一个Animal
类和一个Dog
类,那么编译器将会编译出这样的命令:
#构造函数的代码片段,表示生成实例实体时的命令 execute in minecraft:overworld run summon marker 0 1 0 {Tags:[example_class_animal_pointer,mcfpp_classObject_just],data:{pointers:[],functions:{makeSound:"example:_animal/make_sound"}}} execute in minecraft:overworld run summon marker 0 1 0 {Tags:[example_class_animal_pointer,example_class_dog_pointer,mcfpp_classObject_just],data:{pointers:[],functions:{makeSound:"example:_dog/make_sound"}}}"
第一个命令是生成Animal
类的,第二个则是生成Dog
类的。可以看到,在data.functions
下面,储存了一个键值对,键名是方法名,而值则是这个实例应当调用的方法。那么在调用的时候,我们只需要让这个值作为function
命令的参数,就可以达到多态的效果了。