层次结构修饰符控制类元素在其类层次结构中的行为方式。
默认情况下,.Net 中的所有方法都是非虚的,这意味着它们不能被子类中相同名称的方法(以及签名--参数和返回类型)所覆盖。在下面的示例中,从父类内部的代码中调用方法,结果是调用类层次结构中该特定类定义的方法的版本,而不是子类中定义的具有相同名称和签名的方法(即使我们正在测试的对象是子类的实例):
FUNCTION Start() AS VOID
LOCAL oTest AS Child
oTest := Child{}
oTest:DoTest()
CLASS Parent
METHOD SomeMethod() AS VOID
? "父类方法被调用"
METHOD DoTest() AS VOID
SELF:SomeMethod() // 调用父类的方法,而不是子类中定义的同名方法
RETURN
END CLASS
CLASS Child INHERIT Parent
METHOD SomeMethod() AS VOID
? "子类方法被调用"
END CLASS
这种行为与 Visual Objects、FoxPro 和 Xbase++ 的行为不同,在 Visual Objects、FoxPro 和 Xbase++ 中,所有方法都被认为是虚的。为了使一个方法可以从子类中覆盖,需要将其定义为 VIRTUAL。在下面的代码中,从父类的代码中调用 VIRTUAL 方法,结果是调用子类中定义的方法版本,因为子类已经重载了父类的方法:
CLASS Parent
METHOD NonVirtualMethod() AS VOID // 不能在子类中被覆盖
? "调用了父级非虚方法"
VIRTUAL METHOD VirtualMethod() AS VOID // 能够覆盖
? "父类虚方法被调用"
METHOD DoTest() AS VOID
SELF:NonVirtualMethod() // 调用父类中的方法
SELF:VirtualMethod() // 调用子类中的方法
RETURN
END CLASS
CLASS Child INHERIT Parent
METHOD NonVirtualMethod() AS VOID // 虽然名称相同,但它与父方法完全不同
? "子类非虚方法被调用"
OVERRIDE METHOD VirtualMethod() AS VOID // 会重载父类中的相同命名方法
? "子类虚方法被调用"
END CLASS
请注意,子类中的 VirtualMethod() 在声明时使用了 OVERRIDE 修饰符,这就告诉编译器,我们有意重载了同名的父类方法。在 X# 中,OVERRIDE 修饰符并不是强制性的,也可以省略,但使用它可以使代码更具自明性,并允许编译器进行额外的编译时检查。如果在一个方法上使用 OVERRIDE 来覆盖一个非 VIRTUAL 的父类方法,或者该方法根本不存在(或在父类中拼写了不同的名称或具有不同的签名),那么编译器将报错。因此,建议使用 OVERRIDE 关键字明确声明覆盖父类方法的方法。还可以通过启用编译器选项 “Enforce OVERRIDE”(强制覆盖)(/enforceoverride),在编译器中强制使用 OVERRIDE 关键字。
在上述代码中,编译器确实对子类中定义的 NonVirtualMethod() 报告了警告: "警告 XS0108:‘Child.NonVirtualMethod()’ 隐藏了继承成员‘Parent.NonVirtualMethod()’,如果打算隐藏,请使用 new 关键字。这是对子类中与父类中的非虚方法同名的方法定义发出的警告,因为这可能是意外造成的(要么是意外使用了相同的名称,要么是忘记将基本方法定义为虚方法)。为了告诉编译器,在子类方法中使用同名方法是有意为之(并防止出现警告),可以使用 NEW 修饰关键字,明确标记该方法确实覆盖了基类方法:
CLASS Child INHERIT Parent
NEW METHOD NonVirtualMethod() AS VOID // 明确标记该方法为新方法,与父方法不同
? "子类非虚方法被调用"
OVERRIDE METHOD VirtualMethod() AS VOID // 会重载父类中的相同命名方法
? "子类虚方法被调用"
END CLASS
对于从 Visual Objects 等旧系统移植过来的现有代码(这些系统中的方法总是虚的),在所有需要的方法中手动添加 VIRTUAL 修饰关键字可能会很累。因此,为了方便起见,可以使用 "所有实例方法都是虚方法" (/vo3)编译器选项,指示编译器自动将所有方法都视为虚拟方法。但我们强烈建议您在审查代码时,只在真正需要的地方手动添加 VIRTUAL 修饰符。
请注意,子类中的方法只有在与父类方法的签名完全相同的情况下才能覆盖父类方法,这意味着子类方法的名称(甚至名称的大小写也完全相同,如果启用大小写敏感(/cs)编译器选项)、参数数量和类型以及返回类型都与基类方法完全相同。如果这两个方法在这些方面有任何不同,那么它们将被视为完全不同的方法,其中一个不能覆盖另一个:
CLASS Parent
VIRTUAL METHOD VirtualMethod(n AS INT) AS VOID // 子类的方法名称相同,但签名不同
END CLASS
CLASS Child INHERIT Parent
OVERRIDE METHOD VirtualMethod(c AS STRING) AS INT // 编译器错误 XS0115:“VirtualMethod”:未找到可覆盖的合适方法
RETURN 0
END CLASS
VIRTUAL/OVERRIDE/NEW 修饰符不仅适用于方法,也适用于属性或 ACCESS/ASSIGN 对(也适用于事件,但在声明/覆盖虚事件时意义不大):
FUNCTION Start() AS VOID
Child{}:DoTest()
CLASS Parent
VIRTUAL PROPERTY TestProp AS STRING GET "parent"
ACCESS TestAccess AS STRING
RETURN "parent"
METHOD DoTest() AS VOID
? SELF:TestProp // 子类中重载了该属性。
? SELF:TestAccess // 父级,因为访问不是虚的
RETURN
END CLASS
CLASS Child INHERIT Parent
OVERRIDE PROPERTY TestProp AS STRING GET "child"
NEW ACCESS TestAccess AS STRING
RETURN "child"
END CLASS
最后,类字段也可以声明为新字段(但不是虚字段),以便与父类中的同名字段区分开来:
FUNCTION Start() AS VOID
Child{}
CLASS Parent
EXPORT cField := "parent" AS STRING
CONSTRUCTOR()
? SELF:cField // 指的是父类中定义的字段
END CLASS
CLASS Child INHERIT Parent
NEW EXPORT cField := "child" AS STRING
CONSTRUCTOR()
SUPER()
? SELF:cField // 子类,指的是父类中定义的字段
END CLASS
请注意,XBase++ 方言也有修饰符:
DEFERRED |
ABSTRACT 的同义词 |
INTRODUCE |
NEW 的同义词 |
SYNC |
方法内的代码受到保护,不会在不同线程中同时运行 |
对一个类使用 SEALED 修饰符,可以防止它被子类继承。
这对于防止类/库的用户在子类中改变其功能并可能引入问题非常有用。
它还能让编译器生成更高效的代码,因为编译器知道一个类型不会有子类。
SEALED CLASS Parent
END CLASS
CLASS Child INHERIT Parent // 编译器错误 XS0509:不能从 SEALED 类型 “Parent ”派生
END CLASS
SEALED 也可用于特定(虚)方法/属性,以防止在子类中仅重写这些方法/属性:
FUNCTION Start() AS VOID
Child{}:DoTest()
CLASS Base
VIRTUAL METHOD VirtualMethod() AS VOID
? "base"
METHOD DoTest() AS VOID
SELF:VirtualMethod() // 父类,因为方法在父类中可以被重写,但在父类的子类中不能被重写
END CLASS
CLASS Parent INHERIT Base
SEALED OVERRIDE METHOD VirtualMethod() AS VOID
? "parent"
END CLASS
CLASS Child INHERIT Parent // compiler error
// OVERRIDE METHOD VirtualMethod() AS VOID // 编译器错误 XS0239:无法覆盖方法,因为它是 SEALED 类型
NEW METHOD VirtualMethod() AS VOID // 只能定义一个具有相同名称和签名的新方法,该方法不能覆盖父方法
? "child"
END CLASS
可以在类上使用 ABSTRACT 修饰符,以防止类被意外地直接实例化。只有从抽象类继承(并实现附加功能)的类才能被实例化。这对于实现基本功能但无法直接创建实例的类(如 System.Windows.Forms 中的 Control 类或 VOSDK 中的同名类)非常有用:
FUNCTION Start() AS VOID
LOCAL o AS OBJECT
o := Child{} // OK
o := Parent{} // 编译器错误 XS0144:无法创建 ABSTRACT 类型的实例
ABSTRACT CLASS Parent
END CLASS
CLASS Child INHERIT Parent
END CLASS
抽象类还可以定义抽象方法或属性,这就要求继承自抽象类的类必须实现这些方法或属性。抽象方法是隐式虚拟的,因此可以被重写,如果不在子类中提供实现,就会造成编译器错误:
ABSTRACT CLASS Parent
METHOD BasicFunctionality() AS VOID // 子类中无需重载
ABSTRACT METHOD MustImplementInChild() AS VOID // 必须在子类中定义
END CLASS
CLASS Child INHERIT Parent
// 省略这一点会导致编译器错误,即 Child 类没有实现继承的抽象方法
OVERRIDE METHOD MustImplementInChild() AS VOID
END CLASS
请注意,XBase++ 方言使用的修饰符 FINAL 相当于 SEALED。X# 不支持 FREEZE 修饰符。