在火山平台中进行面向对象的程序设计

2021-6-18 大约 25 分钟

# 在火山平台中进行面向对象的程序设计

注: 基于火山安卓平台举例

# 定义包

每新建一个火山程序,都会在首部固定有一个不可被删除的包定义成员:

img

在“包名”列中直接填入你所自己的包名即可。名称可以随意填写,可以与其它火山程序所使用的包名一致,此时说明这些火山程序均位于同一个包中,也就是说,这些相同包名的火山程序中定义的类均具有相同的包名前缀。

包名中间可以包括句点,通常使用的名称格式为以组织或事物的从大到小排列,譬如:“湖北.武汉.递归公司”、“递归公司.软件开发部”、“火山系统.安卓平台.测试程序”等等,这些都可以,自己维护这些代码时觉得清晰易懂就行了。

如果不需要使用当前火山程序又不想把它从工程中删除,可以在名称前面加上“//”文本(“//”为火山系统的注释引导文本)将其在编译时屏蔽掉:

img

在程序中的所有各类成员定义表格中均可进行类似处理,只要在名称前加上“//”文本,那么该定义实体及其中的所有内容在编译时就会被全部屏蔽掉。

在“属性名”和“属性值”列中,可以填入对应当前所定义成员的系统属性(以‘@’字符开头,除非特意指出,一般用户无需了解)或者用户属性(仅针对数据类型为成员变量/局部变量有效,由该类的可写成员属性提供,见后)。

# 定义类

在编辑器中点击鼠标右键,选择“插入->插入新类”即可在当前程序所处包中插入一个新的类:

img

同样,在“类名”列中填入你所期望的名字就行了,与包名不同的是:

  1. 同一个包中不允许出现相同的类名;
  2. 类名中不允许使用句点。实际上包名是唯一允许中间使用句点的名称。

如果你的类使用了基础类,同样在“基础类”列中填入该类的名称即可:

img

此时,当前类将自动继承所有来自所指定基础类的内容,如果该基础类还有基础类,将一并继承过来。

类定义中还有一个“公开”列,用作定义类对外提供的访问权限。它是一个勾选列,被勾选表示为真,未被勾选表示为假。下同。

如果你勾选了“公开”,那么这个类在你的程序的任意地方都可以被使用,如果你没有勾选,那么这个类只能在它所处包的内部程序中被使用。

譬如,如果保持前面的“保温电水壶类”的公开列被勾选,那么下面的代码就可以正常使用:

img

如果你将“保温电水壶类”的公开列取消勾选,那么由于“测试类”所处的包“火山.测试”与“保温电水壶类”所处的包“湖北.武汉.甲公司”不一致,箭头所指向的代码就会编译失败。

当然,如果“测试类”也位于“湖北.武汉.甲公司”包里面,则还是可以访问的。

在程序中的使用方法和基本数据类型譬如“整数”、“文本型”是一样的,任何可以使用基本数据类型的位置都可以使用

两者之间唯一的不同在于:有子成员,而基本数据类型没有子成员。

譬如上图中的代码定义了一个“保温电水壶类”的“我的电水壶”对象实例,那么我们就可以使用“我的电水壶.壶盖”访问它的“壶盖”成员变量,使用“我的电水壶.烧水()”调用它的“烧水”方法。

在类的对象实例名称和欲访问的子成员名称之间使用句点分隔即可,前面指定具体访问哪一个对象实例(也就是说,假设有很多个电水壶,我要去使用哪一个),后面指定访问这个对象实例的具体哪个子成员。

# 定义类成员变量

成员变量用作类存储其数据时使用。

在类中点击鼠标右键,选择“插入->插入新成员/局部变量”即可在当前类中插入一个新的成员变量:

img

在“成员变量名”列中填入该成员变量的名称,要求在其所处类及其所处类的所有基础类中均唯一。

在“类型”列中填入该成员变量的数据类型,可以是基本数据类型譬如“整数”、“文本型”等,也可以是

公开”列用作定义类成员变量对外提供的访问权限:如果“公开”设置为真,表明该成员变量在所有程序位置都可以被访问,如果为假,表明该成员变量只能在其所处类或者其所处类的继承类中被访问。

譬如:前面“电水壶类”中的“壶盖”和“壶体”成员变量:

img

由于其“公开”未被设置,因此其只能在“电水壶类”及其继承类“保温电水壶类”中被访问,在程序中的其它位置均无法访问则两个成员变量。

其它类别的类成员(譬如方法、属性、事件)的“公开”列均是本作用,下面就不再累述。

成员变量的“静态”列如果被勾选,表明该变量为静态成员变量。静态成员变量有以下特点:

  1. 静态成员变量并未存放在类的对象实例中,而只存放在类本身中,所以无论定义多少类对象实例,静态成员变量都只存在一份;

  2. 由于静态成员变量存放在类本身中,所以在其所处类/继承类外部需要以“所处类名.静态成员变量名”的方式访问。

    img

    如上图,在“测试类 1”中定义了一个名为“静态成员变量 1”的成员变量,在测试类 2 中红色箭头所指向的代码是正确的访问方式,黄色箭头所指向的代码虽然也能被编译器所接受,但是由于不是推荐的访问方式会提示警告。

    无论在“测试方法”方法中定义了几个“测试类 1”的对象实例(“对象 1”、“对象 2”),“测试类 1”的“静态成员变量 1”始终只存在一份,因此红色和黄色箭头指向的代码所访问的“静态成员变量 1”都是同一个。

成员变量的“参考”列如果被勾选,表明该变量为参考变量。参考变量有以下特点:

  1. 参考变量本身并不会定义对象实例,而是用作保存指向其它对象实例的“参考”,访问这个变量等于访问这个变量所参考到的另一个对象实例;
  2. 由于其本身并未定义对象实例,因此参考变量在使用前必须首先赋值(即赋予其所参考到的对象实例)。

想一想我们常用的耳机的插头,它就是“参考变量”的一个例子:当它连接电脑时,我们可以听到电脑播放的声音,当它连接手机时,我们可以听到手机播放的声音,这个“连接”动作就代表了对插头这个“参考变量”进行赋值( 指定其参考到电脑或手机)。当它未连接任何设备时(即未参考到任何对象实例时),当然什么声音都听不到。

请查看如下代码(通过局部变量演示,两者“参考”属性的作用是一样的):

img

首部定义了“电水壶对象 1”和“电水壶对象 2”两个“电水壶类”的对象实例,然后定义了一个名为“电水壶参考对象”的“电水壶类”参考变量。

两者的区别在于:前者创建了真实存在的对象实例,而后者仅仅用作存放一个参考。

由于“电水壶对象 1”和“电水壶对象 2”存在对象实例,因此后面调用其“烧水”方法不会有问题,而紧跟其后的“电水壶参考对象.烧水()”(红箭头指向)调用就会被编译器报错,因为其尚未设置所欲参考到的对象实例。

在后面通过将“电水壶对象 1”赋值过去,从而将“电水壶参考对象”参考到了“电水壶对象 1”,因此再调用“电水壶参考对象.烧水()”(青箭头指向)就不会出错了,其等效于调用了“电水壶对象 1.烧水()”。

==注意==,最容易犯的错误是遗漏设置必要的“参考”属性,如下图:

img

“方法 1”和“方法 2”中的“变量 1”在定义时均没有设置“参考”属性,这样编译器会自动为其创建一个“测试类”对象实例,但是所创建的该对象实例在程序中根本没有被使用,从而导致程序执行效率降低,还占用了不必要的内存空间。

所以每当我们定义一个数据类型为类的变量时,一定要检查是否需要为其设置“参考”属性 ,判断方法就是编译器自动创建的对象实例有没有在程序中被使用到,如果没有被使用到,就一定要设置“参考”属性。

初始值列仅用作给基本数据类型的变量赋予初始值,譬如:

img

具体可用的基本数据类型及初始值格式见语法手册。

# 定义类成员常量

成员常量用作类存储其恒定不变的数据时使用。

不可被修改的变量”被称为 “常量”,其也是成员变量的一种,因此在前面就没有单独阐述。

在类中点击鼠标右键,选择“插入->插入新常量”即可在当前类中插入一个新的成员常量:

img

常量与变量有以下不同之处:

  1. 常量只能在定义时被赋予初始值,不能在程序中被修改;
  2. 常量的数据类型只能为基本数据类型(注:还可以为常量类,但普通用户不需了解);
  3. 常量“静态”属性固定为真,即:常量并未存放在类的对象实例中,而只存放在类本身中,所以无论定义多少类对象实例,常量都只存在一份;
  4. 由于常量的数据类型只能为基本数据类型,所以“参考”属性对于常量来说没有意义。

常量的访问方式与静态成员变量的访问方式一样,在其所处类/继承类外部需要以“所处类名.常量名”的方式访问。

常量一般用作定义一些不会在程序中被修改的恒定值,譬如圆周率的 PI。这样既能避免多次输入出错,还能提供程序的运行效率(便于优化)。

下图是在火山基本库的“数学运算类”中定义的 2 个数学常量,在需要使用这些值的地方可以使用常量来代替(如“数学运算类.E”、“数学运算类.PI”),避免输入错误:

img

# 定义类成员方法

成员方法用作类对外提供其所支持的功能时使用。

在类中点击鼠标右键,选择“插入->插入新方法”即可在当前类中插入一个新的成员方法:

img

在“方法名”列中填入该方法的名称,要求在其所处类及其所处类的所有基础类中均唯一。

方法的“类别”列可以选择“通常/属性读/属性写/定义事件/接收事件”5 类,本处只讨论“通常”类别,其余 4 种在后面讨论属性事件成员时再阐述。

静态”列为真的方法与静态成员变量基本类似,在其所处类/继承类外部需要以“所处类名.方法名”的方式访问。两者之间不同的是:在静态方法内部,只能访问其所处类或者其所处类的基础类中的成员常量或者静态成员变量。譬如:

img

其中定义了一个名为“测试方法”的静态方法,其中第一行代码同时访问了类中的“静态变量 1”和“常量 1”,这是被允许的,然后后面的一行代码访问了非静态变量“变量 1”(红色箭头指向处),这是不被允许的,编译器将报错。

返回值类型”列指定了方法返回值的数据类型。如果指定了非空数据类型,则方法中就必须调用“返回”关键字语句来返回对应数据类型的数据。

img

注意,为了让用户使用起来更方便,火山程序对一种特定格式静态方法的调用方式进行了优化:

img

譬如上面的程序在“测试类”中定义了一个名为“方法 1”的静态方法,它第一个参数的数据类型为“测试类”自身,那么在程序中其它位置调用这个方法时,可以采用以下两种方式:

  1. 测试类.方法1 (变量1, 1)
  2. 变量1.方法1 (1)

第一种方式是标准的静态方法访问方式,第二种就是优化后的方式,其第一个参数被移动到方法访问对象上,这种方式在编译时将被自动转换为第一种方式。

采用这种优化方式的具体要求为:

  • 静态方法第一个参数的数据类型必须为其所处类本身。

只要满足这个要求,该静态方法被调用时第一个参数就可以被移动到其方法访问对象上,其后续参数正常填写。

可以为方法定义一个参数表,用作指定调用此方法时所需要提供的参数:

img

需要注意的是:

  1. 数据类型为的参数始终以参考方式传递对象。譬如前面的“参数 1”**,**如果在外部调用“**测试方法 (电水壶对象1, 123)**”,那么“参数 1”将是指向“**电水壶对象 1”**的参考,操作“参数 1”等效于操作“电水壶对象 1”
  2. 调用方法时,必须加上用小括号括住的参数表,即使该方法的参数表为空,也必须加上用小括号括住的空参数表。如,假设前面的“测试方法”没有定义任何参数,调用它时也必须使用“**测试方法()**”。

# 定义类成员属性

成员属性用作表达或修改类的特征时使用。

成员属性根据其可访问方式分为 3 类:“可读成员属性”、“可写成员属性”、“可读写成员属性”。

# 定义“可读成员属性”:

可读成员属性”用作支持对属性值的读取。

插入一个方法,将其“类别”列设置为“属性读”,然后保证其满足以下格式要求:

  1. 访问权限必须为“公开”;
  2. 不能为静态方法(实际上静态方法也可以用作定义属性,不过这个不需要普通用户掌握,有兴趣可以去查看语法手册);
  3. 没有参数;
  4. 必须定义有一个返回值,该返回值的数据类型即为该属性的数据类型

譬如以下代码在“测试类”中定义了一个数据类型为“整数”的可读属性“我的属性”:

img

注意:虽然“我的属性”是以成员方法的形式定义的,但是在程序中访问时不能以方法的访问方式“对象.我的属性()”来访问,而应该以访问“成员变量”的方式来访问,即:“对象.属性名”。

如,访问上面可读属性的语句为:

img

其中黄色箭头指向处定义了“测试类”的一个对象实例,红色箭头处读取了其“我的属性”的属性值并将其赋值到“整数变量 1”。

实际上,读取“测试类对象.我的属性”时,等效于调用了“我的属性”属性读取方法。

# 定义“可写成员属性”:

顾名思义,“可写成员属性”就是用作支持对属性值的写入。它的定义方式与定义“可读成员属性”类似,只是所定义方法的格式要求不同。

插入一个方法,将其“类别”列设置为“属性写”,然后保证其满足以下格式要求:

  1. 访问权限必须为“公开”;
  2. 不能为静态方法(同上);
  3. 只有一个参数,该参数的数据类型即为该可写属性的数据类型
  4. 没有返回值。

譬如以下代码在“测试类”中定义了一个数据类型为“整数”的可写属性“我的属性”:

img

同样,虽然此处“我的属性”是以成员方法的形式定义的,但是在程序中访问时不能以方法的访问方式“对象.我的属性(欲写入的属性值)”来访问,而应该以访问“成员变量”的方式来访问,即:“对象.属性名 = 欲写入的属性值”。

如,访问上面可写属性的语句为:

img

其中黄色箭头指向处定义了“测试类”的一个对象实例,红色箭头处将其“我的属性”的属性值赋值为 123。

实际上,写入“测试类对象.我的属性”时,等效于调用了“我的属性”属性写入方法。

可写成员属性”还有另外一种访问方式,就是直接在对象的属性表中设置,如:

img

等效于前面通过语句“测试类对象.我的属性 = 123”对该属性的写入。

操作小提示:要想知道当前对象有哪些“可写属性”,在“属性名”列上按下空格即可:

img

# 定义“可读写成员属性”:

很容易理解,“可读写成员属性”就是既可以被读取又可以被写入的属性,定义它的方式也很简单,就是同时定义名称相同的属性读属性写方法,如:

img

其中黄色箭头指向定义了“我的属性”的属性读方法,红色箭头指向定义了“我的属性”的属性写方法。

前面已经讲过,“可读属性”用作支持对类属性的读取操作,“可写属性”用作支持对类属性的写入操作,编译器会根据当前操作是读取还是写入自动调用对应的属性读/写方法。当以以下方式对“我的属性”进行访问时:

img

黄色箭头指向处将调用“我的属性”的属性读方法,而红色箭头指向处将调用“我的属性”的属性写方法。

定义“可读写成员属性”时需要注意的是:

  • 属性读方法”和“属性写方法”的数据类型必须一致,也就是说:“属性读方法”的返回值与同名“属性写方法”第一个参数的数据类型必须一致:

    img

    如图中黄色和红色箭头所指向的数据类型必须一致。

# 定义“可读写成员变量属性”:

有时候我们想直接把一个“成员变量”同时定义为“成员属性”,譬如前面的例子所定义的“我的属性”也可以这样定义:

img

具体方法就是为该成员变量设置属性值为的“@属性变量”系统属性(系统属性是指以'@'字符开头的系统预定义属性)即可。

操作小提示:要想知道当前对象有哪些“系统属性”,在“属性名”列上输入'@'字符即可:

img

注意:这些系统属性,除了在本文档中提到的,一般用户无需了解。

此处定义的“我的属性”,与前面通过“属性读/写方法”定义的“我的属性”没有任何区别,同样可以在对象的属性表中使用:

img

那么问题来了,既然可以这么简单地定义成员属性,那么为什么还要那么麻烦去通过“属性读/写方法”来定义呢?

道理很简单:通过“属性读/写方法”来定义可以使用程序代码对属性的读/写操作进行具体控制,还可以进行一些额外的特定操作,而定义“成员 变量属性”就没办法达到这个目的了,本处两者效果一致只是一个特例。

# 定义“只读成员属性”和“只写成员属性”:

定义“只读成员属性”和“只写成员属性”很简单:

  • 只提供了“属性读方法”的属性就是只读属性,只提供了“属性写方法”的属性就是只写属性

如果想对“只读成员属性”进行写操作,或者想对“只写成员属性”进行读操作,编译器都会报错:

img

如上图,在“测试类”中定义了名为“我的只读属性”的只读属性(未提供该名称的属性写方法)和名为“我的只写属性”的只写属性(未提供该名称的属性读方法),那么在黄色和红色箭头所指向处的代码在编译时都会报错。

可以通过此方式对属性的读写权限进行分别控制。

# 定义类成员事件

成员事件用来类对外发送通知时使用。

一个很简单的例子:用作“按钮”的类必须在用户单击按钮时向外部发送“被单击”事件,用作“时钟”的类必须向外部定时发送“时钟周期”事件,等等。

类的其它三类“成员变量”、“成员属性”、“成员方法”都是被动接受来自外部的访问,而“成员事件”是主动向外部发送通知,这是两者之间的最主要不同。

  1. 定义 “成员事件”:

    插入一个方法,将其“类别”列设置为“定义事件”,然后保证其满足以下格式要求:

    1. 访问权限必须为“公开”;
    2. 不能为静态方法;
    3. 返回值必须为整数;
    4. 方法体必须为空。

    事件定义方法对参数表没有要求,用户可以根据自己的需要随意定义。

    譬如我们先前所定义的“电水壶类”中的“水烧开警告”事件:

    img

  2. 在类中发送事件:

    在类中的代码内,当需要发送事件时,直接调用该事件的“事件定义方法”即可,如前面例子中的:

    img

    当调用“事件定义方法”时,如果该事件定义方法上挂接了对应的“事件接收方法”(见下),会自动去调用该“事件接收方法”并返回其所返回的整数值,否则会直接返回整数值 0。

  3. 接收其它类所发送过来的事件:

    要想接收其它类所发送的事件,必须首先定义相应的事件接收方法,譬如以下代码在“测试类”中定义了一个“电水壶类”的对象:

    img

    想要接收其“水烧开警告”事件,需要如下操作:

    鼠标右键单击“电水壶对象”的定义行:

    img

    选择其中的“添加电水壶对象的事件接收方法”菜单项:

    img

    再选择其中的“电水壶类_水烧开警告”,然后单击“添加”按钮,会自动在程序中插入对应的事件接收方法:

    img

    当然,你也可以自己手工创建并填写符合此格式的方法,效果是一样的。

    查看上面所生成的“事件接收方法”,可以发现它的格式要求:

    1. 方法名称必须为:“事件定义方法所处类名”+下划线+“事件定义方法名称”;
    2. 不能为静态方法;
    3. 方法的第一个参数必须为固定的“来源对象”参数,其数据类型为发送事件的类,用作提供具体是哪个对象发送过来的事件;
    4. 方法的第二个参数必须为固定的“标记值”参数,其数据类型为整数,用作动态挂接事件时使用(见后);
    5. 方法的后续参数表必须与对应的“事件定义方法”一致,用作提供在事件定义方法所处类中发送事件(调用该事件定义方法)时所传递过来的具体参数值;
    6. 方法的返回值必须为整数,此返回值将被传递回在事件定义方法所处类中调用该事件定义方法的调用方。

    一旦为类成员变量对象的“事件定义方法”定义了对应的“事件接收方法”,那么该事件就被自动挂接到了此接收方法上,在事件定义方法所处类中一旦调用该“事件定义方法”,此“事件接收方法”就会被自动调用。

    具体例程请参见系统安装目录的“samples\beginner\beginner.vsln”解决方案中的“火山面向对象设计”项目**。**

    附:这种特性是不是比其它编程语言更强大?不再是只有在被设计窗体上的窗口组件才能发送事件了,也不再需要为了让其能发送事件去开发类似“时钟”这样的哑窗口组件了。在火山中,任何代码位置处的对象均可以发送事件。

  4. 动态挂接其它类所发送过来的事件:

    如前所述,一旦为类成员变量对象的“事件定义方法”定义了对应的“事件接收方法”,那么该事件就被自动挂接到了当前类中的对应“事件接收方法”上,但是其它代码位置处对象的事件是不会自动挂接到当前类中的,譬如下面代码:

    img

    在“测试方法”中定义了一个“电水壶类”的局部变量对象,此时该对象上的“水烧开警告”事件是不会自动挂接到当前类的“电水壶类_水烧开警告”事件接收方法上的。也就是说,当前类此时将无法接收到来自此“局部电水壶对象”的“水烧开警告”事件。

    如果需要接收该局部变量对象的事件,必须调用“挂接事件”关键字明确挂接其事件到当前类:

    img

    挂接事件(局部电水壶对象)”语句被执行后,此“局部电水壶对象”的“水烧开警告”事件就被挂接到了当前类的“电水壶类_水烧开警告”事件接收方法上,以后当前类就可以接收到来自该对象的“水烧开警告”事件了。

    在调用“挂接事件”关键字时可以额外再提供一个标记值参数,如:“挂接事件(局部电水壶对象,123)”,此时该“局部电水壶对象”一旦发送事件,事件接收方法的“标记值”参数将接收到此处所提供的标记值“123”,便于程序中对此事件进行特定处理。

上次编辑于: 2021年6月18日 19:35