类
创建
在项目浏览器上,点击鼠标右键,选择 "新建 -> 类",就可以创建一个新的类:
我们给它起个名字叫做 “Parent”,因为我们将要使用它当 做另一个类的父类。创建新类的时候,LabVIEW 会询问新类继承自哪里:
作为我们创建的第一个类,我们并没有为它准备一个父类。LabVIEW 中所有的类都必须有且仅有一个父类,如果不需要设定一个特别的父类,那么就会默认使用 “LabVIEW 对象” 作为父类。所以 “LabVIEW 对象” 会是 LabVIEW 中所有类的祖先类,当你编写了一个 VI,用于处理所有 LabVIEW 的对象,比如得到对象的类名,那么输入数据的类型就可以采用 “LabVIEW 对象”,使得你的程序可以接受任何类型的实例。
接下来我们再使用同样的方法创建一个名为 “Child” 的类,它是 “Parent” 的子类,所以选择 “Parent” 作为它的父类:
类在结构上是一种特殊的 LabVIEW 库,因此它的很多属性和设置都与库相似。比如,类的名字也作为名字空间;也可以为类中的 VI 设置访问权限等。除此之外,类还有它特殊的设置,比如有属性和方法等。
类被保存在一个以 lvclass 为后缀名的文件中。
方法(VI)
鼠标右键点击在类上,就可以为类创建方法了。方法其实就是一些 VI。
在新建这一栏下可以看到很多条目:
- VI:就是指创建一个普通的方法 VI。
- ** 虚拟文件夹 **:如果类中的方法很多,为了便于管理,可以把它们归类到不同的文件夹中。
- ** 属性定义文件夹 **:这是专用来保存数据读写 VI 的文件夹。
- ** 基于动态分配模板的 VI**:如果一个类中的方法,有可能被子类中相同的方法重写,就应该使用这个模板。相当于其它语言中的 “虚函数”。
- ** 基于静态分配模板的 VI**:如果一个类中的方法,不允许被子类重写,就应该使用这个条目来创建。它与基于动态分配模板的 VI 的唯一的区别在于:动态分配的 VI 的类输入输出接线端是动态分配的,而静态分配的 VI 则不是。
- ** 用于数据成员访问的 VI**:因为类的数据全部是私有的,所以需要借助公有 VI 来访问它们。这个选项用于快速建立读写类中数据的 VI。这些 VI 依然是基于动态分配模板的 VI 或基于静态分配模板的 VI,只是 LabVIEW 帮忙在程序框图上添加了一些数据读写的代码。
- ** 用于重写的 VI**:这个选项是专门给子类用的,用来创建覆盖父类的方法 VI。它创建的是一个基于动态分配模板的 VI,只是 LabVIEW 帮忙在程序框图上添加了一些调用父类同名方法的代码。
- ** 类型定义 **:创建用户自定义控件,用于自定义一些在模块里可能需要用到的数据类型。
下面我们研究一下 “基于动态分配模板的 VI” 和 “基于静态分配模板的 VI” 各自的行为是什么样的。
首先在 Parent 类中创建一个基于静态分配模板的 VI,叫做 static.vi。这个 VI 的功能比较简单,只是返回一行文字 “Parent Static VI”。
之后我们尝试在 Child 类中创建一个同名的基于静态分配模板的 VI。这时候就会发现新建的 Child.lvclass:static.vi 是不能运行的。
点击它的运行按钮,可以看到错误信息:它试图覆盖一个祖先类中的基于静态分配模板的 VI。
如果一个类中已经有了一个基于静态分配模板的 VI,它的子孙类中就不能再有同名的方法了。
再创建两个基于动态分配模板的 VI,我们可以在父类和子类中分别创建出同名的基于动态分配模板的 VI。它们的区别仅在于输出文字略有不同,两个 VI 都可以正常运行:
现在我们可以写一个测试程序来看看上面几个 VI 都会返回什么结果?下图是一个简单的测试,它的程序框图上分别有一个父类的实例,和一个子类的实例,之后分别把它们传递给上面创建的几个 VI,看看返回结果:
上图中,偏紫色图标的 VI 都属于 Parent 类,偏黄色图标的 VI 都属于 Child 类。
static.vi 由于是基于静态分配模板的 VI,不能被子孙类覆盖,所以可以确定,被调用的永远是 Parent 类中的那个 static.vi,返回值也一定是 “Parent Static VI”,不论输入的类型是哪一种。
Parent 类的实例调用 Parent.lvclass:dynamic.vi 返回值是 “Parent Dynamic VI”;Child 类的实例调用 Child.lvclass:dynamic.vi 返回值是 “Child Dynamic VI”。这也是比较确定的。
需要注意的是最后一条测试,“message 6” 中的文字。因为 Child 类继承自 Parent 类,Child 类可以被认为是 Parent 类的一个子集。如果一个对象属于 Child 类,也就必然属于 Parent 类。因此在程序中我们可以把这个 Child 类的对象的数据类型转化成为 Parent 类的数据类型,然后用它去调用 dynamic.vi。这里的实例是由 Child 类生成的,不论它在程序中使用哪个祖先类的数据类型表示它,它都始终还是一个 Child 类实例,所以程序调用它的 dynamic.vi,运行的一定还是 Child 类中的那个 dynamic.vi。我们可以看到这里返回的文字是 “Child Dynamic VI”。只有当子类中没有实现(重写)某个基于动态分配模板的 VI 时,程序才会调用它父类中的同名 VI。
下面我们再改动一下 Parent.lvclass:static.vi 的程序逻辑,让它去调用一下 Parent.lvclass:dynamic.vi:
然后我们再 在测试程序中,使用一个泛化成 Parent 类类型的 Child 类的实例调用这个 Parent.lvclass:static.vi。返回值会是什么呢?
尽管 Child 子类中没有 static.vi,测试程序确定调用的是父类中的 static.vi,但是由于传入的实例属于 Child 类,那么父类的 static.vi 中调用的依然是 Child 类中的 dynamic.vi。一个基于动态分配模板的 VI,即便是在其它类的 VI 中被调用,依然还是会运行实例所属的类中的方法。
这也就是面向对象编程中 “多态的含义”。我们在编写程序的时候可以总是在代码里放置父类中的方法,但在程序运行时,调用的是哪个方法是根据传入的实例的具体类型来决定的。如果传入的对象属于某一个子类,被调用的将会是那个子类中的方法。换句话说,这个被调用的父类的方法是抽象的,它会根据传入对象具体类型的不同表现出多种形态,所以叫做多态。
如果一个父类的 VI 被子类覆盖之后,是不是就无法在子类中被调用了呢?在子类中调用父类同名方法时,不能如同调用一般的子 VI 那样,直接把父类的方法拖拽过来。这种调用方式是无效的,而必须使用函数选板中的 "编程 -> 簇,类与变体 -> 调用父类方法" 节点来调用父类的方法。如果在新建类的方法时,选择 “用于重写的 VI”,系统会自动把这个节点加在新生成的 VI 程序框图上。
下图中的方法 VI 使用了调用父 类方法节点,先得到父类同名方法返回的数据,再把这个数据与子类方法自己的数据合并返回:
基于静态分配模板的 VI 与基于动态分配模板的 VI 在源代码层面的区别仅仅是:动态分配的 VI 的类输入输出接线端是动态分配的,而静态分配的 VI 则不是。如果已经创建了一个基于静态分配模板的 VI,但是后来发现它应该是基于动态分配模板的 VI,那么也不需要重新在生成一个新的 VI,只要改变原有 VI 的接线端的类型就可以了。
属性 (数据)
除了 VI,每个类都包含一个和类同名的.ctl 项。尽管它的面板和设置方法与用户自定义控件类似,但实际上在硬盘上是找不到这个.ctl 文件的。它的数据信息都直接记录在同名的.lvclass 文件中。此外,这个.ctl 项必须是一个簇。簇中的元素就是这个类的属性,也就是类所使用到的所有数据,这相当于其它语言中的类的变量。与多数其它语言不同之处是,LabVIEW 的类的数据只能是私有的。这主要是出于安全考虑。在类之外,只能通过公有的方法来间接访问这些数据。
因为数据都是私有的,所以也不存在继承的问题,子类不能继承父类的数据。如果子类需要使用父类里的数据,也需要通过调用父类提供的方法间接调用。
我们可以为刚刚创建的类添加一些数据:
子类和父类中可以添加同名的数据。类的数据可以有初始值设定,初始值就是对应控件的默认值。上图中,如果 message 控件的默认值是空字符串,那么新创建出来的实例中 message 的数据都是空字符串;如果把 message 控件的默认值改为字符串 “init”,那么新创建出来的实例中 message 的数据都是 “init”。
我们可以使用类的 “新建 -> 用于数据成员访问的 VI” 菜单创建一些用于读写数据的 VI。
新建的数据访问 VI 也可以选择是动态的或是静态的。因为数据都是私有的,不会有继承的问题,使用静态的数据成员访问 VI 会更直观一些。唯一的问题是,如果父类和子类中,有重名的数据,它们的数据成员访问 VI 也会重名,这样如果是静态的 VI,就会遇到我们上一节提到的那个 “试图覆盖一个祖先类中的基于静态分配模板的 VI” 的错误。这时候可以给数据访问 VI 改个名,VI 的名字不是必须和数据名相同的。
另一种解决方案是创建动态的数据成员访问 VI,这样父类子类中的数据成员访问 VI 可以重名。还有一个额外的好处是子类中访问父类的数据会更容易理解。LabVIEW 的数据不能继承,但现实中有些情况下数据是应当被继承的,比如说父类是 “家具”,有个属性 “价格”,子类是 “桌子”,显然 “桌子” 应当继承 “家具” 的价格。在遇到这种情况时,我们可以在父类和子类中添加同名的数据,生成同名的动态数据成员访问 VI,以方便在子类动态数据成员访问 VI 中直接调用父类的同名数据成员访问 VI,从而得到父类中的数据。
下图显示了我们为测试项目创建的一些数据成员访问 VI:
类中的数据成员访问 VI 也可以通过 LabVIEW 的属性节点来调用,使用属性节点访问数据的好处是一次可以读写多个属性:
在设计类的时候应该尽量避免把数据成员访问 VI 直接给用户使用。模块的一个设计原则就是尽量把底层数据隐藏起来不让用户看到,用户必须通过模块提供的高层方法进行操作,这样模块的开发者才可能放心的对模块底层结构和数据进行维护改进。
数据线样式
默认状态下,LabVIEW 中每个新创建的类,都使用了相同样式和颜色的数据线。但是为了在演示程序中更清楚的分辨一根数据线传递的是哪一个类的数据,演示程序为每个类都设置了不同的数据线颜色和样式:
应用示例
下面我们使用一个更贴近实际生活的例子来演示一下面向对象编程的整个流程。
需求
假设有一家家具店,店里只卖两种家具,桌子和椅子。我们编写一个程序来模拟一下家具店里这些家具的属性和方法。假设我们需要模拟如下的属性和方法:
- 编号(属性):每个家具都有一个唯一的编号。
- 成本价(属性):商店进货的价格。
- 返回售价(方法):每个家具都有一个固定的售价,它等于
家具的成本价格 *(预计利润率 + 1)*(税率 + 1)
。假设成本价格,利润率和税率都是已知的。 - 组装(方法):桌子和椅子组装过程。
- 桌子的组装:把桌腿固定在桌面上,然后反转过来。具体过程在演示程序中用一段文字代替即可。
- 椅子的组装:把靠垫和椅背固定在一起,然后再安装椅子腿。具体过程在演示程序中用一段文字代替即可。
- 以上几个属性和方法都是桌子椅子共有的,此外,我们再要求每个类提供一个自己独特的方法:
- 铺桌布(桌子类的独特方法):在演示程序中用一段文字表示桌布铺好即可。
- 放置靠枕(椅子类的独特方法):在演示程序中用一段文字表示靠枕放好了即可。
此外,我们将编写一个模拟程序,调用家具店中每一个家具,打印出它们的售价和承重。