第五章 类与对象
类(Class)是Fuxi程序设计语言的基本编程单位。类是对象的结构,同时也是根据其定义来创建对象的机制。类包含属性(常量、字段)、构造函数、方法(函数、谓词和触发器)。类支持继承(Inheritance),继承是派生类扩展并限定其基类功能的机制。
类声明(Class declaration)定义一个新的引用类型,并且描述其实现。类声明出现在其它类中的类称为嵌套类(Nested Class);而其它非嵌套类称为顶级类(Top-level Class)。
本章讨论所有类的公共文法和语义,它们包括顶级类、嵌套类(包括成员类、局部类和匿名类)。
类可以被声明为公开的(public)或内部的(internal),表明其是一个模块的输出类,可以被别的模块调用;否则,为私有类,只能为本模块中的其它类所使用。
类可以带有风格。
一个命名类(Named Class)可以被声明为抽象类(Abstract class),并且对于那些没有完整实现的类必须是抽象类;抽象类不能被例化,但可以是其它类的基类。类也可以被声明为终结类(final class),终结类不能作为其它类的基类,并且必定不能是抽象类。
类可以声明为主动类(active class),主动类的所有实例都是主动式对象。类可以声明为本地类(native class),即本地机器代码实现的类,本地类的定义体类不含类的实现(但它也不是抽象的,因为其实现是在本地代码中定义)。类也可以是持续性类(persistent),该类的所有实例都具有持续性。
类可以是可移动的(mobile),该类的所有实例都是可移动的,可移动类也必定是主动类。持续性类必定是不可移动,因此persistent和mobile不可混用。
当一个类声明过大时,可以把类声明分解成若干段。这是每一段的类声明都必须带partial风格;只有这样,编译器才可以将类的几个部分合并成一个完整的类定义。
§5.1 类声明
类声明语句声明一个新类,其文法如下:
<类声明>定义如下:
[<封装修饰符>]{<风格修饰符>} class
<类名> [<基类表>] <类定义体>
<类名>定义如下:
<标识符>
或
<标识符>.<类名>
<基类表>定义如下:
<类类型>
或
<接口类型表>
或
<类类型> , <接口类型表>
<接口类型表>的定义如下:
<接口类型>{,
<接口类型>}
<类定义体>的定义如下:
{{<类成员声明>} {<构造函数定义>}}
<封装修饰符>是下列关键字之一:
public, internal, protected, private
<风格修饰符>定义为下列之一:
<实现风格修饰符> 或
<对象风格修饰符>
<实现风格修饰符>是下列关键字之一:
abstract, final, native, partial, static
<对象风格修饰符>是下列关键字之一:
active, mobile, persistent
其中<封装修饰符>、<风格修饰符>及<基类表>都是可选项。最简单的类声明包括三个部分:class、声明的类名(<标识符>)和<类定义体>。类声明可以不带<封装修饰符>,其缺省<封装修饰符>为private;如果带<封装修饰符>,则选择<封装修饰符>中的一个,<封装修饰符>的组合是没有意义的,是非法的。而<风格修饰符>可以组合使用,但abstract和final,mobile和persistent的组合是非法的。
§5.1.1 类的封装性
类声明中可以使用public或internal关键字,来表明所声明的类是一个输出类。输出类可以被其它模块使用。一个可执行的Fuxi程序必须至少包括一个public输出类。internal和public输出类不同,public类可以被远程调用,而internal类仅能被本地的模块使用,因此internal类通常只出现在Fuxi类库中。
类声明中未使用public或internal的类,为私有类,仅能在本程序中使用,不能被其它程序调用。
§5.1.2 类的继承性
§5.1.3 类的风格
Fuxi语言一个重要的特点是正交的对象风格化(Orthogonal Object Stylization)。对象可以拥有风格,系统提供风格的实现。类声明中也可以使用风格,表明此类定义的所有对象都具有此风格。
这里对风格进行以下说明:
-
abstract风格:具有abstract风格的类是抽象类,抽象类中可以包含抽象方法(没有实现的方法)。由于不存在抽象的对象,抽象类不能被例化。
-
active风格:具有active风格的类是主动类,主动类的所有实例都是主动式对象。每个主动式对象都拥有一个对立的执行线程。
-
final风格:final仅在类声明中使用,具有final风格的类为终结类,不能作为其它类的基类。
-
mobile风格:Fuxi在语言级支持移动计算,具有mobile风格的对象为可移动对象。当远程程序发出调用请求时,可移动对象将迁移到调用者所在的站点上,供调用者使用。
-
native风格:具有native风格的类是本地实现类,其定义体中不包含方法的实现(但它也不是抽象类),其方法的实现是在本地代码中定义的。native风格的类的所有实例都是本地实现的外部对象,都和本地的机器相关。native对象遵守Fuxi
Generic Object规范,可以为Fuxi抽象机调用。
-
persistent风格:具有persistent风格的对象为持续性对象。持续性对象都是不可移动的,因此persistent和mobile不能混用。
类是对象的静态模板,除了abstract、final和native三个标注其实现状态或方式的风格外,其余风格均为对象风格,表示该类的所有实例均具有这些风格。除了abstract、final和native三个类实现风格不具有继承性外,其余对象风格都具有继承性,也就是说,一个具有某种对象风格的类,其派生类也具有这些风格。
为了保持数据的完整性和一致性,类的active、mobile和persistent风格必须具有可继承性,也就是说,一个具有这些风格的类,其派生类也必须具有这些风格。
为了提高程序的可读性,继承来的类风格必须在类声明中加于标注,否则认为是语法错误而被拒绝。例如:
public active class TestApplication : Application
{
......
}
由于Application类为主动类,其派生类TestApplication也必定是主动类,因此TestApplication的声明中必须使用active关键词;否则,编译器将发出错误信息。
§5.1.4 抽象类和终结类
无完整实现的类为抽象类(abstract class);和抽象类相对的是具体类(concrete class)。抽象类必须是某个类的基类,否则不能被例化;而终结类(final class)不能作为任何类的基类,终结类也必定不是抽象类。
抽象类具有如下特点:
-
抽象类不能被例化。如果在程序中使用抽象类来定义对象,就会出现语法错误;
-
抽象类不能同时为终结类;
-
抽象类中可以包含一个以上抽象方法(abstract method),但不是必须的;
-
一个抽象类的派生类如果没有实现该抽象类中的所有抽象方法,则其该派生类也必须是抽象类。
请看下面的例子:
// 抽象类例子 AbstractClass.lj
abstract class BaseClass
{
public GetValue( ) = int
public SetValue( int x ) = bool
}
abstract class AbstClass: BaseClass
{
protected int x
public GetValue( ) = x
}
final class Positive : AbstClass
{
public SetValue( int x ) = if( x > 0 ) this.x := x else this.x := 0
}
final class Negative : AbstClass
{
public SetValue( int x ) = if( x < 0 ) this.x := x else this.x := 0
}
在上面的代码中,由于BaseClass类中包含两个抽象函数GetValue和SetValue,因此BaseClass为一个抽象类。在AbstClass中虽然实现了BaseClass中的一个抽象函数GetValue,但仍然有一个抽象函数SetValue没有实现,因此AbstClass也是一个抽象类。Positive和Negative类实现了基类中的所有抽象函数。
§5.1.5 本地实现类
本地类(Native Class)是指由本地的机器代码实现的类。程序中,本地类只是这些本地代码的调用模板或接口,本地类的定义体中不得包含任何实现代码。虽然本地类的定义体中,不含方法的实现,但本地类并不是抽象类或接口,因为类的实现存放在某个本地代码库中,执行时将会被抽象机调用,来完成该类所规定的全部功能。
本地类也可以是抽象类,可以带有抽象方法,这时,在程序中必须作为某个类的基类才能被例化;本地类也可以是终结类。
§5.1.6 主动类与Fuxi程序的执行
带有active风格的类称为主动类,主动类的所有实例都是主动式对象。active风格可以被继承,也就是说,如果一个类的基类是主动类,则其必须是主动类,因此,其类声明中必须带active关键字。
Fuxi是一种并发式语言,其并发机制采用actor模型,主动式对象就是一个actor。每个主动式对象都是一个独立运行的线程,拥有自己独立的运行堆栈。非主动式对象称为被动式对象,每个被动式对象必定是某个主动式对象的子孙(descendent object)。在Fuxi的执行系统中,最外层的根对象必定是一个主动式对象。主动式对象内部的被动式对象之间的调用是直接通过方法调用(Invocation)进行,而主动式对象之间则是通过消息传递(message passing)实现的。
Fuxi的交互模型(Interaction model)采用的是异步的消息传递(Asynchronous Message-Passing)。每个主动式对象都有一个消息队列,其执行线程依次处理这些消息,并把处理的结果存放到由消息发送方提供的Future单元内。消息的发送方在发送完消息后,便继续进行后面的计算。但计算需要引用调用的返回值,即Future单元的值时,如果此时消息的接收方已经处理完消息,并把返回值存放到Future单元中,则直接使用Future中的值,并继续执行;否则,消息的发送方进入阻塞状态,等待Future单元中的返回值。
当一个Fuxi程序模块被装载时,Fuxi抽象机便开始创建根对象,进行完初始化后,并为其分配相应的线程。分配完线程后,执行主动式对象的第一个函数Activate(),这是一个不带参数的bool型函数,如果Activate返回true,便进入消息循环,否则线程执行结束。
§5.1.7 可移动类
带有mobile风格的类为可移动类。可移动类,并不是指类本身可移动,而是指该类的所有实例都是可移动对象。
§5.2 类成员
类成员声明定义一个类成员,其文法定义如下:
<类成员声明>定义为:
<常量定义> 或
<字段声明> 或
<方法声明> 或
<类型声明>
<方法声明>定义为:
<规则式声明> 或
<运算符声明>
<类型声明>定义为:
<类声明> 或
<接口声明>
类成员不仅包括该类声明时类定义体中定义的成员,同时也包括从其基类中继承来的成员。总的说来,类成员包括以下8个部分:
序号 |
类成员 |
说 明 |
1 |
常量 |
它表示与类相关的常量值 |
2 |
字段 |
这是类的变量 |
3 |
函数 |
实现类中可以执行的计算 |
4 |
子句 |
类中反向规则 |
5 |
事实 |
|
6 |
触发器 |
类中的正向规则 |
7 |
运算符 |
定义了可以对该类实例应用的运算符操作 |
8 |
类型 |
定义了属于类的局部类型 |
Fuxi语言是一个逻辑函数型语言,Fuxi类中的方法较C++、JAVA等过程性语言要丰富。Fuxi的方法包括函数、子句、事实和触发器,分别实现计算、反向推理和正向推理功能。
声明类的同时也创建了一个新名字空间(Namespace),并且类声明包含的所有类成员声明的新成员都被放入此新名字空间内,所以类成员声明必须符合下列原则:
-
类成员不得与该类的名字相同;
-
类的下列成员如常量、字段及子类型的名字不能与该类中的其它成员同名;
-
类成员的方法如函数、子句和触发器不能与其它非类型的方法成员同名。
§5.2.1 成员的封装性
封装性(Encapsulation)或访问控制(Access Control)是面向对象技术的一个重要特征。Fuxi语言提供以下四种成员修饰符来提供四个不同级别的访问控制:
private, protected, internal, public
-
Private:私有成员,只有类成员可以访问得到;
-
Protected:受保护的成员,只有类及其派生类的成员可以访问;
-
Internal:内部公开的成员,可以被同一程序内部的任何对象访问;
-
Public:完全公开的成员,可以为任何对象(包括远程对象)访问。
Fuxi中类成员缺省为私有成员,我们可以根据需要,通过下面两种方法来改变成员的可视性。
第一种方法,可在成员声明前直接使用上述的封装修饰符关键字,声明该成员的封装性。这是一种简单而直接的方法。例如,
class Point
{
int x
int y
protected SetPos( int x, int y ) =
{
this.x := x
this.y := y
}
public GetPosX( ) = x
public GetPosY( ) = y
}
在这个例子里,x和y为私有成员(因为缺省为私有成员),而SetPos为受保护成员,GetPosX,GetPosY为公开成员。
第二种方法,设置封装段。一个封装段从<封装段引导符>开始,到下一<封装段引导符>结束。
<封装段引导符>定义为:
<封装修饰符>:
封装段内的类成员的缺省封装为<封装修饰符>所指定。封装段的范围限制于类定义体内,因此当类定义结束,封装段也自然结束。例如,
class Point
{
private:
int x, y
public:
protected SetPos( int x, int y ) =
{
this.x := x
this.y := y
}
GetPosX( ) = x
GetPosY( ) = y
}
在本例中,Point类有两个封装段。成员x和y处于private段内,其访问控制为private。成员SetPos、GetPosX、GetPosY处于public段内。由于SetPos受其前面的protected控制,因而为protected成员,而GetPosX和GetPosY为缺省的public成员。
§5.2.2 静态成员
静态成员(Static Member)是与类相关联的,是该类所有实例共享的成员。静态成员在类装载时就进行初始化。静态成员的定义体内(如果有的话),不能包含对本类中的非静态成员的访问。
静态成员的声明中,必须包含关键字static。
看下面的例子,
// Static Member
import fuxi.*
class StaClass
{
protected static int x
public int y
public static IncX() = x++
public IncY() = y++
}
public active class StatApp : Application
{
StaClass a, b
public Activate( ) =
{
a.IncX()
a.IncY()
b.IncX()
b.IncY()
Console.Println(
"b.x = " + b.x )
Console.Println(
"b.y = " + b.y)
}
}
该程序的执行结果为:
b.x = 2
b.y = 1
由于x为StaClass的静态成员,为a和b两个实例所共享,执行中被两次+1,因此为2。而y不是静态成员,b.y仅进行了一次+1计算,因而值为1。
Fuxi语言不支持全程变量。如果确实要定义全程变量,可将这些全程变量以静态成员的形式封装在某个类中。例如,
// Global Variables
import fuxi.*
class Globals
{
public static int x
public static f( int x ) = this.x + x
}
class A
{
public SetX(int x) = Globals.x := x
}
class B
{
public SetX(int x) = Globals.x += x
}
public class GlobApp : Application
{
A a
B b
public Activate( ) =
{
a.SetX(10)
b.SetX(10)
Console.Println( "Global x = " + Globals.x )
Console.Println( "Global f(10) = " + Globals.f(10) )
}
}
程序执行结果为:
Global x = 20
Global f(10) = 30
本例中我们将全程变量x和函数f(x)作为Globals类的两个公开静态成员封装在Globals中,供其它类中的函数使用。
§5.2.3 常量
常量是那些在编译时就已经确定的数值或对象,而且在执行过程也保持不变的类成员。每个常量都是静态的,是和类相关的成员。常量声明为我们声明一个常量,其文法定义如下:
<常量声明>定义为:
[<封装修饰符>] const <类型> <标识符> =
<常量表达式>
<常量表达式>定义为:
<数值表达式> 或
<对象表达式> 或
[]
或
[<常量表达式>{,<常量表达式>}]
<类型>为Fuxi所支持的类型,包括基本类型,如int,double等,和引用类型,如数组、程序中已定义的类等。<常量表达式>必须是同<类型>兼容的表达式;同时,<常量表达式>必须是在编译时就可以确定值的表达式,否则,会视为语法错误。常量总是静态的,因此没有必要使用static关键字。
以下是一个常量的例子:
class Colors
{
public const uint 黑色 = 0x000000
public const uint 红色 = 0x0000ff
public const uint 绿色 = 0x00ff00
public const uint 兰色 = 0xff0000
public const uint 黄色 = 红色 | 绿色
public const uint 洋红 = 红色 | 兰色
public const uint 青色 = 绿色 | 兰色
public const uint 白色 = 红色 | 绿色 | 兰色
public const uint[3] 三元色 = [红色,绿色,兰色]
}
§5.3 字段
字段(Field),也称为成员变量,它是类的数据成员,是与类或对象相关的变量。字段声明定义一个类的字段,其文法如下:
<字段声明>定义为:
[<封装修饰符>]{<风格修饰符>}<类型><标识符>[<字段定义体>]
或
[<封装修饰符>]{<风格修饰符>}<类类型><标识符>( <参数表> )[<对象定义体>]
<字段定义体>的文法是:
[ =<初始化表达式>] [ <约束体> ]
或
[
:=<初始化表达式>] [ <约束体> ]
<风格修饰符>定义为:
<实现风格修饰符> 或
<对象风格修饰符> 或
<自定义风格修饰符>
<实现风格修饰符>定义为以下关键字之一:
final static volatile
<对象风格修饰符>定定义为以下关键字之一:
active mobile remote persistent
其中<封装修饰符>、<风格修饰符>和<字段定义体>(Field
declarator)都是可选项;字段的声明中的<标识符>为字段名。一个最简单的字段声明只有<类型>及其名字<标识符>。
§5.3.1 字段的类型
字段声明中的<类型>的文法定义如下:
<类型>定义为:
<基本类型>
或
<用户类型>
或
<带精度限制的基本类型> 或
<带长度限制的用户类型> 或
<集合类型>
<基本类型>定义为:
<整数类型> 或
<浮点类型> 或
<字符类型> 或
<逻辑类型>
<整数类型>为下列关键字之一:
byte short int long ubyte ushort uint ulong
<浮点类型>定义为:
float 或
double
<字符类型>定义为:
char
<逻辑类型>定义为:
bool
<用户类型>定义为:
<类类型> 或
<接口类型>
<带精度限制的基本类型>定义为:
<整数类型><<整数>>
或
<浮点类型><<整数>,<整数>>
<带长度限制的用户类型>定义为:
<类类型><<整数>>
<集合类型>的定义为:
<类型><集合后缀>
<集合后缀>的形式为:
[] 或
[<整数>] 或
[]<<整数>>
§5.3.1.1 类型及其子类型
Fuxi语言允许对的基本类型进行精度/数位长度控制,从而形成了基本类型的子类型(subtype)。子类型只出现在字段/变量的定义中,不能使用子类型来进行强制类型转换。当表达式对某个子类型字段/变量进行访问时,其值将自动转换成其对应的类型,换句话说,就是表达式不接受子类型。当对字段/变量进行赋值时,系统会验证所赋的值是否属于字段/变量子类型所规定的范围,来确定赋值是否能进行。
数位长度指数值的十进制数的长度,对于带符号数,此长度还包含1位符号。例如:
int<8> 表示 -9,999,999 ~ 9,999,999
uint<8> 表示 0 ~ 99,999,999
而对于浮点数其第1个整数表示整个数位的长度,其中还包括1位符号和1位小数点,而第2个整数表示小数部分的长度。例如,
double<12,3> 表示的范围是:-9,999,999.999 ~ 9,999,999.999
数位长度的限制不可大于其类型本身的最大数位长度,否则会视为文法错误,例如:
byte<6> int<12>
就是两个错误的类型定义。
对于具有可变长度的类类型(例如String),也可以使用长度限制,例如对String进行长度限制:
String<8>
表示长度不超过8的String子类型。对于绝大多数的具有固定长度的对象采用长度限制是没有意义的,会破坏数据的完整性。
§5.3.1.2 集合类型
集合类型是指某一类型数据的集合,Fuxi中集合类型分为表(List)和数组(Array)两种形式。表是具有可变长度的集合类型;而数组的长度是固定,并且在数组建立时,其元素也相应建立。
集合类型的定义形式是:类型定义加集合后缀。集合后缀[]表示任意长度的表,而[]<<整数>>表示最大不超过<整数>长度的表;后缀[<整数>]表示长度为<整数>的数组。Fuxi语言允许定义任何类型数据的集合类型,以下都是合法的集合类型:
int[] float[8] int[]<10>
int<4>[] int<4>[8] double<12,3>[4][]<50>
其中int[]表示int型的表类型,int[]<10>表示长度不超过10的int表,int<4>[]表示数位长度不超过4位的int型的表,而int<4>[8]则表示具有8个长度不超过4位的int型元素的数组。
§5.3.2 字段的风格
Fuxi的字段可以使用风格。风格是和类型相正交的字段属性,类型规定了字段/变量的数值范围,而风格规定了字段的某些实现方式或运行姿态。例如,static风格表明字段是和类相关联的,为类的所有实例所共享;final风格表示字段一旦被赋值或约束,则这个值便是其最终值,将不再改变;而volatile风格则和final相反,表明具有这种风格的字段的值时刻都有变化的可能。
字段的风格分为实现风格和对象风格。实现风格是和字段自身实现相关的风格,而对象风格是该字段所引用的对象所应具有的风格。这里我们先讨论字段的实现风格,而对象风格将在有关对象的章节中讨论。
§5.3.2.1 static字段
在字段声明中,带有static风格的字段称为静态字段,有时也称类变量,它是和类关联的变量,为类的所有实例所共享。而其它非静态字段也称实例变量,它是和类的实例相关的变量。
§5.3.2.2 final字段
一旦拥有值后,这个值就不能改变的字段,称为final字段。一个final字段要么带初始化表达式,要么在构造函数中被赋值,实例中不允许存在自由的final字段。同样,final字段也必定不是volatile的,final和volatile不能混用。
在程序的执行过程中对final字段的赋值是非法的,将返回false。
约束变量缺省带有final风格,除非在字段声明中,显式地声明其为volatile的。
§5.3.2.3 volatile字段
字段的内容会因其它字段的内容变化而变化,我们可以将这样的字段设为volatile。当对volatile字段进行访问时,如果该字段是约束字段,就需要对约束表达式进行重新计算,因为前次计算的结果可能已经“挥发”掉了。
Volatile风格不能和final及persistent合用。
§5.3.3 字段的指称及初始化
Fuxi是一个说明性语言,表达式也是一个数据对象,而且是一类数据对象(first class data object),即表达式可以作为参量在函数间进行传递。当一个变量和某个表达式建立起关联后,这个变量到底代表什么呢?是代表这个表达式本身,还是代表这个表达式的值呢?表达式是一个程序段,在执行过程中程序是不能改变的;而值是某个内存单元中的内容,在程序的执行过程中可以被某个程序段修改。
变量与数据对象之间的映射关系,称为变量的指称。Fuxi语言中,字段/变量具有三种指称状态,即无指称(自由变量)、指称表达式(约束变量)和指称值(值变量)。字段/变量的这种指称关系是通过初始化来设定的。字段/变量的初始化分为约束初始化和赋值初始化。没有初始化的变量是自由变量;自由变量可以被赋值而成为值变量,也可以被约束为某个同类型的表达式。已约束的变量不能再被赋值,而已赋值的变量不能再约束。
§5.3.3.1 字段的赋值初始化
赋值初始化的基本形式为:
:= <表达式>
在字段/变量后添加上述初始化形式,字段/变量就成为了一个值字段/变量。系统将会为这个字段/变量分配一个能存放得下其值的内存空间,并把值放到这个内存单元中。此时,字段/变量就表示这个内存单元。并且该内存单元的位置在字段/变量的生命期内保持不变,虽然内存单元内值可能会发生变化。
§5.3.3.2 字段的约束初始化
约束初始化的定义形式为:
= <表达式>
在字段/变量名后,使用上面的初始化形式,字段就被约束到<表达式>上。初始化之后,字段的值就是表达式的值。在程序的整个执行过程中,字段始终等同于这个表达式。
当字段/变量被约束到某个表达式上时,并不意味着要马上就对表达式进行计算。Fuxi语言是一种惰性语言,对约束变量值的计算被推迟到当其它计算需要引用该字段的值时,才进行计算。理解Fuxi语言这种惰性计算语义非常重要。
§5.3.4 字段的约束体
<约束体>是对字段设置的约束条件,这种约束条件称为卫兵;带<约束体>的字段称为受卫字段(Guarded Field)。<约束体>的文法为:
<约束体>定义为:
{ [<前卫规则>] [<后卫规则>] }
<前卫规则>定义为:
Before()-> <表达式>
<后卫规则>定义为:
After()-> <表达式>
Before()和After()是两个触发器,也称事件,其中的<表达式>均为bool型。在对字段进行赋值操作的前后,系统将分别触发这两个触发器。
当对字段进行赋值操作时,系统将首先检查该字段是否包含Before触发器,如果没有Before触发器的话,立即进行赋值操作。如果包含Before的话,将计算->后的<表达式>的值,如果该值为true,则进行赋值操作,否则赋值失败。
但完成赋值操作后,系统将检查该字段是否定义了After触发器,如果没有定义,赋值操作完成。如果用户定义了After触发器,则计算其<表达式>的值,如果值为false,系统将恢复字段原来的值,赋值操作失败。
字段的<约束体>包含两个方面的意义,一方面可以用来约束字段的值域,保证字段的取值正确,另一方面也可以利用这两触发器向其它对象发消息,通知它们其值的变化。
请看下面的例子,
import fuxi.*
class SafeVal
{
int x
{
Before() -> y <= 50
After() -> x + y <= 100
}
int y
{
After() -> x + y <= 100
}
public SetValue( int x, int y ) =
{
this.x := x
this.y := y
}
}
public active class SafeApp: Application
{
SafeVal val
public Activate() = {
if(
val.SetValue( 120, 0 ) )
Console.Println("1: 成功赋值: (" + x + ',' + y + ')' )
else
Console.Println( "1: 赋值失败!" )
if(
val.SetValue( 20, 80 ) )
Console.Println( "2: 成功赋值: (" + x + ',' + y + ')' )
else
Console.Println( "2: 赋值失败!" )
if(
val.SetValue( 30, 70 ) )
Console.Println( "3: 成功赋值: (" + x + ',' + y + ')' )
else
Console.Println( "3: 赋值失败" )
Console.Println( "最终值为: (" + x + ',' + y + ')' )
}
}
程序执行的结果为:
1: 赋值失败!
2: 成功赋值: (20, 80)
3: 赋值失败!
最终值为s: (20, 80)
程序中val的初值为(0,0),第一次执行SetValue(120,80),在对x进行赋值操作前,Before被触发,结果为true,进行赋值(x=120),赋值操作后触发After触发器,结果为false(x+y=120),赋值失败,x恢复原值0。第二次执行SetValue(20,80),先对x赋值,这是y=0<50,赋值后x=20,x+y=20,赋予值成功;接着对y赋值,y无Before触发器,赋值后y为80,After条件为true,成功。而第三次执行SetValue(30,70)时,因为y>50,x:=30失败。
§5.4 构造函数
构造函数(Constructor)是用来对类或类的实例进行初始化的函数。其特性如下:
-
构造函数的名称必须与类的名称相同,这样编译器才能在成员函数中分辨出构造函数来。
-
构造函数的性质与其它用户定义的函数的性质相同,但构造函数的值必须是bool型。如果用户定义一个名称与类的名称相同,但函数值不是bool型,编译器将视为错误。
-
一个类可以有一个以上的构造函数,即构造函数的重载。只要构造函数的参数个数或参数类型不同,编译器将视为不同的构造函数。用户可以按照此规则为一个类设计多个构造函数。一个没有任何参数的构造函数称为缺省构造函数(Default Constructor)。
-
构造函数不是类的成员,因此不具有继承性。虽然构造函数在类的定义体中定义,但构造函数不是类成员,不管其封装属性如何,类的派生类都不继承该构造函数。同时其它类成员也不可以直接调用构造函数。
我们来看一个构造函数的例子。
import fuxi.*
class Grade
{
String m_strName
int m_nGrade
// 构造函数
public Grade( String strName, int nGrade ) =
{
m_strName := strName
m_nGrade := nGrade
}
public Grade() =
{
m_strName := "无名氏"
m_nGrade := 0
}
getName() = m_strName
getGrade() = m_nGrade
isPass() = m_nGrade >= 60
toString() = "学生: " + m_strName + " " +
switch
{
case m_nGrade >= 90: "优秀"
case m_nGrade >= 80: "良好"
case m_nGrade >= 70: "中等"
case m_nGrade >= 60: "及格"
default: "不及格"
}
}
public active class GradeApp: Application
{
Grade stu1( "林黛玉", 100 )
Grade stu2( "薛宝钗", 90 )
Grade stu3( "史湘云", 85 )
Grade stu4( "贾宝玉", 50 )
public Activate( ) =
{
Console.Println( stu1.toString() )
Console.Println( stu2.toString() )
Console.Println( stu3.toString() )
Console.Println( stu4.toString() )
}
}
§5.4.1 构造函数的调用
在程序中用定义好的类来定义或创建一个对象时,则构造函数就会被自动调用(如果该类有构造函数的话),也就是说,构造函数是由系统根据情况自动调用,并不是用户来调用。正如前面所述,所有构造函数的值是bool型,系统在创建一个对象时,该类的某个构造函数将被调用,如果构造函数的值为true,说明对象被成功构造;如果构造函数的值为false,对象构造失败,同时分配给该对象的资源也将释放。
Fuxi表达式中允许使用类名加构造参数的方法来创建一个对象,但此时调用的并不是构造函数本身。这种调用方法完全等价于:
<类>.new( <参数表> )
看下面这个表达式:
"今天距离春节还有" + (Date( "2003.1.22" ) - Date.GetToday()) + '天'
请注意表达式中的Date( "2003.1.22" )。Date是lang库中的一个类,Date( String strDate )是Date类的一个构造函数。而表达式中的Date( "2003.1.22" )返回的是一个Date的实例,而不是true。其实,表达式中的Date( "2003.1.22" ) 完全等价于Date.new( "2003.1.22" )。
函数new是由系统实现的类的一个静态方法,用来创建该类的一个实例,即对象。函数new的执行过程是,根据类的信息为新实例分配一定的内存空间,根据new的参数,寻找合适的构造函数来初始化这个实例。如果初始化成功,即构造函数返回true,new函数将返回该实例的引用,如果初始化失败,即构造函数返回false,系统将释放刚分配的空间,同时new返回一个空引用。
§5.4.2 静态构造函数
静态构造函数是类的初始化函数,而非静态的构造函数称为实例构造函数。当系统创建类对象时,将调用该类的静态构造函数,用于对类中的静态成员进行初始化。静态构造函数不得带有参数,且必须使用static显式地声明为静态。静态构造函数的表达式中不得含有对非静态成员的引用。
看下面的例子:
import fuxi.*
class C1
{
static C1() = Application.Console.Println( "初始化类C1" )
public static F() =
Application.Console.Println( "调用C1.F" )
}
class C2
{
static C2() = Application.Console.Println( "初始化类C2" )
public static F() =
Application.Console.Println( "调用C2.F" )
}
public active class TestApp: Application
{
public Activate() =
{
C1.F()
C2.F()
}
}
这段程序经编译执行,读者将会发现这段代码的输出结果是:
初始化C1
初始化C2
调用C1.F
调用C2.F
从这个例子中我们可以看出,静态构造函数总是在类的任何其它成员被调用之前被系统自动地调用。
§5.4.3 缺省构造函数
如果一个Fuxi类没有声明任何构造函数,系统将自动地为其提供一个缺省的构造函数。缺省构造函数只是简单地调用该类的直接基类的无参数构造函数。但是,如果该类的直接基类没有可调用的无参数构造函数,就会出现运行错误。另外,一个抽象类的缺省构造函数的访问特性为protected,其它类的构造函数的访问特性为public。
§5.4.4 私有构造函数
如果一个类的构造函数仅有一个,并且被声明为private,则该类不能作为其它类的基类,同时该类也不能被例化。private构造函数通常只用于那些只有静态成员的类。例如:
class Globals
{
private Globals() = true // 私有构造函数,防止实例化
public static double PI = 3.1415926
public static Area( double r ) = 0.5 * PI * r * r
public static Radian( double angle ) = angle * PI / 180
}
Globals类为我们提供了一组常量和方法,但它不能被例化。
§5.5 方法
Fuxi中,方法分为函数(Function)、谓词(Predicate)和触发器(Trigger)。一个Fuxi方法通常由若干个规则构成。Fuxi中,具有相同类型的方程式(Equation)定义一个函数,具有相同类型的子句(Clause)定义一个谓词,而具有相同类型的产生式(Production)构成一个触发器。构成某一方法的规则的共同类型,称为该方法的类型,也称为原型(Prototype)。
§5.5.1 方法的原型
Fuxi语言在定义一个方法时,可以先声明该方法的原型。原型声明的文法是:
<原型声明>定义为:
[<封装修饰符>] [<风格修饰符>] <标识符> (<参数类型表>)
<定义符> <类型>
<封装修饰符>可以为以下关键字之一:
public internal protected private
<风格修饰符>可以是以下关键字之一:
abstract const final new override partial static
<定义符>是下列之一:
= -> <-
这里<标识符>为方法名,Fuxi中称为函词(Functor)。
Fuxi中方法的原型声明不是必须的,编译时Fuxi编译器会自动根据规则的类型生成方法的类型,必要时还将进行函数的类型推导。
Fuxi的类声明中,如果声明了一个方法的原型,同时这个方法又不是抽象的,则必须定义这个方法的规则,否则编译时会出语法错误。类中如果某个抽象方法只有原型,而没有相应的规则,则称这样的方法为抽象方法。带有抽象方法的类称为抽象类(Abstract Class),抽象类不能被例化。
§5.5.1.1 方法的封装性
如果程序中为方法声明了原型,则方法原型中的封装修饰符定义了该方法的封装性;如果没有声明原型,则该方法的第1条规则的封装性便是整个方法的封装性。
-
private:私有方法无继承性和可视性,只能被类的其它成员访问。我们不能定义私有的抽象方法或部分实现的方法,因此private方法不可以带abstract或partial关键字;
-
protected:受保护的方法,可以被类成员或子类成员访问。受保护的方法缺省具有final风格,不能被的派生类中的相同名函数所扩展或覆盖,除非在方法中显式地使用partial关键词来声明其是部分实现的方法。
-
internal:内部公开的方法,可以被当前进程中的任何对象访问。除了不能被远程对象所访问外,internal方法具有和public方法相同的特点。
-
public:公开的方法,可以被任何对象访问,包括远程对象的访问。Public和internal方法缺省为partial实现的方法,可以为其派生类中方法所扩展或覆盖,除非在方法原型的声明中显式地使用final关键字。
§5.5.1.2 方法的风格
-
静态函数:带static风格的函数为静态函数,静态函数的定义体中不能包含对类的非静态成员的访问。
我们来看一个河内塔的游戏程序。
有三根柱子,其中的一根柱子上从大到小依次串着n个大小不同的盘子。现在要你把这n个盘子移到另一根柱子上,但要求一次只能移动一个盘子,而且柱子上的盘子必须满足大盘在下条件。
// 河内塔游戏 -- Hanoi.fux
import fuxi.*
public active class HanoiApp : Application
{
Move( int nDisks, int from, int to ) = bool
Move( 0, int from, int to ) = Console.Println( "移动完成" )
Move( 1, int from, int to ) =
Console.Println( "移动:" + from + "==>" + to )
Move( int n, int from, int to ) =
{
Move( 1, from, 6 - from - to )
Move( n - 1, form, to )
Move( 1, 6 - from - to, to )
}
public Activate( ) =
{
Console.Println( "输入盘子数:" )
Move( Console.Readln().ToInteger(), 1, 3 )
}
}
§5.5.2 方法的重载
Fuxi允许编写两个或多个具有相同名字但不同参数的方法,我们称为方法名的重载(Overloading)。前面我们已经讨论过,重载是实现对象的多态性的一种重要的手段,这种多态性又称为静态多态性。当表达式中出现对重载方法的调用时,在编译时就可以确定到底哪个方法被调用。
我们来看一个函数重载的例子。
import fuxi.*
class Rectangle
{
ushort m_usWidth, m_usHeight
public Rectangle( ushort nWidth, ushort nHeight ) =
{
m_usWidth := nWidht
m_usHeight := nHeight
}
public DrawShape( ) = DrawShape( m_usWidth, m_usHeight )
public static DrawShape( ushort nWidth, ushort nHeight ) =
let
{
Iterator i(nHeight)
Iterator j(nWidth)
}
in
{
i.scan
{
j.scan
{
Application.Console.Print( "*" )
}
Application.Console.Println( "" )
}
}
}
public active class RectApp: Application
{
Rectangle rect( 20, 3 )
public Activate( ) =
{
Console.Println( "DrawShape:" )
rect.DrawShape()
Console.Println( "DrawShape(30,2):" )
rect.DrawShape( 30, 2 )
}
}
这个程序的执行结果为:
DrawShape():
********************
********************
********************
DrawShape(30,20):
******************************
******************************
在这个例子中,Rectangle类中的DrawShape方法被重载。应用程序的激活函数(Activate)首先调用了rect的无参数的DrawShape函数,然后又调用了带参数的DrawShape方法。在这个例子我们还看到了Fuxi的循环方法:通过定义循环子对象(Iterator)来实现循环。关于循环子,我们在今后的章节中还将进行详细讨论。
§5.5.3 运算符函数
运算符其实也是一种函数。Fuxi中除了允许方法重载,也允许用户重新定义运算符,以满足程序设计的需要。重载运算符是通过定义运算符函数(operator function)来实现的。运算符函数的文法为:
<运算符函数>定义为:
operator <运算符>( <参数> ) =
<表达式>
其中<运算符>是要重载的运算符,它必须是Fuxi定义的运算符,用户不能自定义新的运算符。
我们来看一个例子:
import fuxi.*
class Complex
{
double re, im
public Complex( double r, double i ) =
{
re := r
im := i
}
public Complex( )
{
re := 0.0
im := 0.0
}
public operator +( Complex c ) = Complex( re + c.re, im + c.im )
public operator +( double x ) = Complex( re + x, im )
public operator - ( Complex c ) = Complex( re - c.re, im - c.im )
public operator - ( double x ) = Complex( re - x, im )
public ToString( ) = re.ToString () + '+' + im + 'i'
}
public active class Complexes: Application
{
Complex c1( 3.0, 2.0 ) // 3 + 2i
Complex c2( 6.0, -1.0 ) // 6 - i
Complex c3 = c1 + c2
Complex c4 = c1 - c2
public Activate() =
{
Console.Println( "c1 + c2 =" + c3.ToString() )
Console.Println( "c1 - c2 =" + c4.ToString() )
Console.Println( "c1 + 3.0 =" + (c1 + 3).ToString() )
Console.Println( "c2 - 3.0 =" + (c2 - 3).ToString() )
}
}
程序执行的结果为:
c1 + c2 = 9.0 + 1.0i
c1 - c2 = -3.0 + 3.0i
c1 + 3.0 = 6.0 + 2.0i
c2 - 3.0 = 6.0 - 1.0i
§5.6对象
类的实例称为对象。
§5.6.1 引用字段与子对象
我们在§5.3中已经讨论字段的声明。如果字段声明中的<类型>为一个类,则这个字段便是一个类型为<类型>的引用字段。引用字段本身并不是该类型的实例,而是对该类型某个实例的引用的变量。如果未对该字段进行任何形式的初始化,则这个字段为一个自由变量。我们知道,自由变量可以被赋值,也可以被约束为一个表达式。我们来看一个例子:
class A
{
int x, y
public A( int x, int y ) =
{
this.x := x
this.y := y
}
public val() = x + y
}
class B
{
int x, y
public A a = A( 10, 10 )
public A b := A( 20, 20 )
public A c
public B( int x, int y ) =
{
this.x := x
this.y := y
}
public val() = a.val() + b.val() + c.val() + x + y
}
在这个例子中,类A在类B中定义了三个字段a、b和c。字段a被约束成表达式A(10,10),字段b被赋值为表达式A(20, 20)的值,而字段c是自由变量。
我们来看一看字段a和b有什么不同。
字段a被约束为表达式A(10, 10),而表达式A(10, 10)的值是一个类型为A的对象的引用。由于约束关系在这个变量的生存期内不变,这说明a表示一个属于类B的一个子对象,它因类B的例化而创建,也因该B的实例的结束而消失。对于直接使用构造函数的子对象可以简化成变量名后加构造参数的形式,即
public a( 10, 10 )
这种定义子对象的形式和C++完全一致。
而字段b是一个值变量,我们可以通过赋值来改变它的内容。这时b所代表的对象不是类B固有的,而是因时而变的,可以在程序执行的过程被赋予其它的值。
类B中包含一个由类A定义的引用字段c,字段c的声明中没有设立初始值,B的构造函数也没有对c进行任何的初始化,因而c为一个自由的引用变量。
§5.6.2 对象的成员初始化
我们接着前一节的例子来讨论。我们应用类B定义新的引用字段:
class C
{
public B b (10, 10)
public val() = b.val()
}
这段程序存在一个问题,类B中的字段c是自由变量,如果调用类B的方法val会出现执行错误。因此在调用方法val前需要对b.c进行初始化。看下面这段程序:
class C
{
public B b( 10, 10 )
public C() = b.c := A( 10, 10 )
public val() = b.val()
}
程序中为类C增加了构造函数,在其构造函数中对其子对象b中的自由字段c进行了初始化。
对象中的自由变量,称为槽(Slot)。槽是指那些在类的字段声明,以及构造函数中都没有进行过初始化的字段。在对象初始化时,常需要程序员往这些槽中填写数据,这些自由变量的名字称为槽名(Slot name),其数据称为槽值(Slot value)。在定义子对象时,我们可以通过成员初始化表为子对象中的槽填写槽值。上面的程序可以改写成:
class C
{
public B b(10, 10) : c(10, 10)
public val() = b.val()
}
也可以写成:
class C
{
public B b(10, 10 )
{
c: 10, 10
}
public val() = b.val()
}
子对象b槽值的第二种表示方法,也称为对象的脚本化定义。这种方法常在图形界面的描述中使用。
我们先来看个例子:
import fuxi.*
import shom.*
class MainSheet
{
public:
base Sheet( 0, 0, 600, 400 )
{
title "表单应用程序"
icon "leaf.ico"
color COLOR_3DFACE
}
Text txt_1( "你好,朋友!" ) : at( 30, 30 )
{
format "@20,40='宋体'%红色"
size 300, 40
}
Picture pic_1( "lotus.bmp" ) : at( 30, 100 )
{
size 100, 100
}
Button btn_1( 2109 ) : at( 400, 20 )
{
text "退出"
size 100, 40
}
}
public class TestApplication
{
public:
base Application
{
style APS_SDIAPP
title "表单应用程序"
icon "leaf.ico"
}
MainSheet m_shtMain
OnCommand( 2109 ) -> if( MessageBox.Show( "确实要结束吗?", "提示信息:",
MessageBox.YESNO | MessageBox.ICONQUESTION )
== MessageBox.IDYES )
Quit( 0 )
}
这是一个简单的表单应用程序。关于表单,我们将在今后的章节中详细讨论。程序中我们首先定义了一个表单MainSheet,它是从Sheet派生出来的。MainSheet中包含三个表单控件(Control):文本(Text)、图片(Picture)和按钮(Button),它们是从shom库中到入的类。MainSheet中的三个控件子对象都采用了对象的脚本化定义方式,填写各自包含的槽中槽值。我们以Text为例来说明:
Text txt_1( "你好,朋友!" ) : at( 30, 30 )
{
format: "@20,40='宋体'%红色"
size: 300, 40
}
Text是shom中定义的类,它包含构造函数Text( String str )和三个自由变量:
Point at
TxtFormat format
Size size
at( 30, 30 )指明Text的位置,为醒目便于同其它控件对照,我们把该槽放在对象的成员初始化表中,而把format和size两个槽放在对象的定义体中。
§5.7 类型转换
Fuxi语言制定了一套数据转换的规则,当程序将某中类型的数值或变量指定给另一种类型的变量时,Fuxi便利用这一规则自动地转换成最后所需要的类型,例如:
long lVar := 16 // int型数16被转换成long型
double dVar := 4 // int型数4被转换成double型
如果程序中的类型转换不符合转换规则,也就是说,两种数据类型不兼容,编译时将出现编译错误。例如,
int nVal := 3.1415926 // double型转换成int型
这是一种错误的赋值转换,编译出现错误。这时可以借助于强制类型转换(type cast)来完成上述的赋值:
int nVal := (int)3.1415926 // double型被强制转换成int型
虽然Fuxi语言本身并不支持基本类型与引用类型之间的转换,但Fuxi支持自定义的类型转换方法。当程序员希望自定义的类型可与其它类型相互转换时,可自行提供这种自动的或强制的类型转换功能。
§5.7.1 基本类型到引用类型的转换
这里我们先来看一个例子:
import fuxi.*
class 时间
{
private long 秒 // seconds
private long 分 // minutes
private long 时 // hours
public 时间( long s ) =
{
时 := s / 3600
分 := (s - hour * 3600) / 60
秒 := s - (hour * 3600 + min * 60)
}
public 时间( long m, long s ) =
{
时 := m / 60
分 := m - hour * 60
秒 := s
}
public 时间( long h, long m, long s ) =
{
时 := h
分 := m
秒 := s
}
public 字符串( ) = 时.ToString() + ':' + 分.ToString() + ':' + 秒.ToString()
}
public active class 比赛成绩: Application
{
时间 李逵 := 13521
时间 燕青 := 时间(192, 50 )
时间 林冲 := 时间(3, 5, 1 )
public Activate() =
{
Console.Println( "比赛成绩:" )
Console.Println( " 李逵:" + 李逵.字符串() )
Console.Println( " 燕青:" + 燕青.字符串() )
Console.Println( " 林冲:" + 林冲.字符串() )
}
}
程序执行的结果为:
比赛成绩:
李逵:03:45:21
燕青:03:12:50
林冲:03:05:01
这个程序用来记录三位梁山英雄的越野马拉松的成绩。程序中的类‘时间’定义了三个构造函数,使得我们能够以三种不同的时间单位来记录成绩。
请注意赋值语句:
时间 李逵 := 13521
变量‘李逵’是一个类‘时间’的对象,而13521是一个整数,这是两个完全不兼容的类型。非但编译没有出错,而且执行也能得出正确的结果。关键就在于类的构造函数。在构造函数中,有一个只有一个参数的构造函数:
时间( long s )
前面的赋值语句完全等价于:
时间 李逵 := 时间(13521)
当某个类定义了单一参数的构造函数后,如果程序中出现对该类型变量赋值需要进行类型转换时,程序会自动地创建一个该类的实例,同时调用其构造函数来完成实例的构造。这样表面上看,仿佛实现了其它类型到该类的类型转换。
§5.7.2 引用类型到基本类型的转换
类中具有单一参数的构造函数仅限于将基本类型转换为用户自定义的类型。要想把引用类型的数据转换成基本类型,需要借助于转换函数(Conversion function)。转换函数可以将用户定义的类型转换成Fuxi基本类型。例如:
int nVar = (int)P1
其中P1为一个引用类型的变量。
转换函数的定义文法为:
<类型转换函数>定义为:
operator <类型>() =
<表达式>
保留字operator后面接所要转换的目标类型,而<表达式>必须具有和目标类型相一致或兼容的类型,转换函数不带任何参数。我们看一个转换函数的例子:
import fuxi.*
class Circle
{
public double dia, len, area
public const double PI = 3.1415926
public Circle( double d ) =
{
dia := d
len := PI * d
area := PI * (d / 2) * (d / 2)
}
public int() = (int)dia
public long() = (long)len
public double() = area
}
public active class ConvApp: Application
{
Circle c1 := 2.0
Circle c2 := 10.0
public Activate() =
{
Console.Println( "圆c1的信息:" )
Console.Println( "直径: " + c1.dia )
Console.Println( "周长: " + c1.len )
Console.Println( "面积: " + c1.area )
Console.Println( "圆c2的信息:" )
Console.Println( "直径: " + (int)c2 )
Console.Println( "周长: " + (long)c2 )
Console.Println( "面积: " + (double)c2 )
}
}
程序执行的结果为:
圆c1的信息:
直径: 2.0
周长: 6.2831852
面积: 3.1415926
圆c2的信息:
直径: 10
周长: 31
面积: 78.53975
|