Show/Hide Toolbars

XSharp

 

在 .Net 中,有两种类型可以保存数据,即引用类型(或类)和值类型(或结构)。这两种类型的使用方式语义不同,但都可以包含其他引用和/或值类型。

类(或引用类型)是最常见的数据存储方式。其名称来源于这样一个事实,即引用类型的变量(通常称为该类型的实例)并不直接包含数据,而是指向(引用)存储实际数据的内存位置。X# 中的类是通过 CLASS...END CLASS 语句 定义的,可以从另一个引用类型继承,实现一个或多个 INTERFACES,并可能包含字段、属性、构造函数、方法、事件和其他项目:

CLASS Customer
  EXPORT name AS INT // exported (public) 字段
  PROTECT age AS INT // protected 字段, 类外的代码看不到
END CLASS

通常情况下,类及其所有成员都定义在一个代码文件中。如果需要在多个文件中定义类成员(例如,当类成员数量非常多时),则必须在每个文件中将类定义为 PARTIAL:

PARTIAL CLASS Customer
// 类成员
END CLASS

由于类的实例只存储一个指向数据的指针,因此一个或多个变量可以指向内存中完全相同的对象。将一个引用类型的变量分配给另一个相同类型的变量,会导致两者代表相同的数据。使用一个引用更新数据时,也会自动更新另一个引用:

FUNCTION Start() AS VOID
  LOCAL one, two AS Customer
  one := Customer{}
  two := one // 现在两个变量都指向内存中的同一个对象
  two:name := "Robert"
  ? one:name // 也是 "Robert"

嵌套类

一个类甚至可以包含其他类型(类或结构)。在这种情况下,主类型内部的类型称为嵌套类型。使用嵌套类型时,可以使用容器类的名称和自己的名称,中间用点连接:

CLASS Customer
  CLASS NestedClass
    EXPORT FieldInNestedClass AS INT
  END CLASS
END CLASS
 
FUNCTION Start() AS VOID
  LOCAL oNested AS Customer.NestedClass
  oNested := Customer.NestedClass{}
  oNested:FieldInNestedClass := 100
  ? oNested:FieldInNestedClass

嵌套类尤其适用于定义辅助类,即只在父类的上下文中使用的类,用于保存只与该类相关的信息。为这些数据创建嵌套类,而不是使用普通类,可以使代码结构更加合理。

结构

与引用类型相比,结构(或值类型)直接存储数据。它与 Visual Objects 的 STRUCTURE 功能(在 X# 中更名为 VOSTRUCT)有一些相似之处,但它比 VO 功能强大得多,因为它可以包含大多数引用类型也有的项目,如属性、构造函数、方法等。与引用类型不同的是,值类型不能从其他类型继承或实现接口。不过它们可以包含嵌套类或结构。值类型可以用 STRUCTURE 语句 定义:

STRUCTURE Vector2D
  EXPORT x AS INT
  EXPORT y AS INT
  METHOD Invert() AS VOID
    SELF:x := - SELF:x
    SELF:y := - SELF:y
END STRUCTURE

由于结构直接保存数据,因此除了数据本身所需的内存(引用类型的数据和数据指针都需要内存)或垃圾收集器活动外,实例化结构不会涉及任何额外的内存消耗。它们大多适合作为轻量级数据容器,通常只容纳少量字段,一般为 2-4 个,但也可以只容纳一个元素,如 System.Int32 (INT) 或 System.Boolean (LOGIC) 数据类型,它们只是定义了 INT 和 LOGIC 数据类型,包括几种操作其数据的方法。其他非常常用的系统定义结构包括 System.Drawing.Point(点)、System.Drawing.Rectangle(矩形)等,它们都包含少量数据字段。

结构语义

与普通类相比,结构在使用时也有不同的语义。因为声明一个值类型的 var 会直接分配其数据,所以不需要实例化这样的变量来使用它:

FUNCTION Start() AS VOID
  LOCAL vector AS Vector2D
  vector:x := 10
  vector:y := 20

不过,为了方便起见,也可以在值类型中定义构造函数,并像普通类一样将它们实例化:

STRUCTURE Vector2D
  EXPORT x AS INT
  EXPORT y AS INT
  CONSTRUCTOR(vec_x AS INT, vec_y AS INT)
    SELF:x := vec_x
    SELF:y := vec_y
END STRUCTURE
 
FUNCTION Start() AS VOID
  LOCAL vector AS Vector2D
  vector := Vector2D{10,20}
  ? vector:x // 10

与引用类型不同的是,两个变量的数据存储在不同的内存位置,对其中一个变量的任何更改都不会影响另一个变量:

FUNCTION Start() AS VOID
  LOCAL vec_1,vec_2 AS Vector2D
  vec_1:x := 10 ; vec_1:y := 20
  vec_2 := vec_1
  ? vec_2:x // 10, 值从第一个 Vector 中复制
 
  vec_2:x := 40 // 将新值加入第二个  Vector
  ? vec_1:x // 10 again, 第一个  Vector 值仍保留其原始值

因此,结构不适合用于非常大的对象,因为将一个对象赋值给另一个对象或将一个对象作为参数传递给方法时,需要将所有数据从源对象复制到目的对象。另一方面,对于普通类,只需将数据指针作为参数传递给方法即可。

等号运算符

引用类型和值类型的另一个重要区别是等号运算符 (==) 的行为。对于引用类型,两个变量之间的等号运算符只比较指针本身,而不比较对象的数据。因此,只有当两个变量指向同一个对象时,它才会返回 TRUE,而在所有其他情况下,即使两个对象包含的数据相同,它也会返回 FALSE:

CLASS ReferenceType
  EXPORT data AS STRING
END CLASS
 
FUNCTION Start() AS VOID
  LOCAL o1,o2 AS ReferenceType
  o1 := ReferenceType{}
  o1:data := "test"
 
  o2 := ReferenceType{}
  o2:data := "test"
 
  ? o1 == o2 // FALSE, 因为 o1 和 o2 指向不同的内存位置
 
  o2 := o1
  ? o1 == o2 // TRUE

另一方面,默认情况下 == 操作符不能在结构上使用,如果尝试使用,编译器会报错。不过,通过在结构中定义一个 OPERATOR 方法来实现与 == 运算符的比较,还是可以使用 == 运算符的。在下面的示例中,实现 == 操作符的目的是比较两个被比较结构所持有的实际数据,因此当数据相等时,操作符将返回 TRUE:

STRUCTURE ValueType
  EXPORT data AS STRING
  OPERATOR == (a AS ValueType, b AS ValueType) AS LOGIC
  RETURN a:data == b:data // 当两个参数的数据相同时,让 equals == 运算符返回 true
END STRUCTURE

FUNCTION Start() AS VOID
  LOCAL o1,o2 AS ValueType
  o1:data := "test"
  o2:data := "nothing"
  ? o1 == o2 // FALSE
  o2:data := "test"
  ? o1 == o2 // TRUE

请注意,可以比较 System.Int32、System.Boolean、System.Double 等大多数常见系统定义结构的值,因为它们也有定义的等号操作符方法,如上面代码中的方法。

使用哪种

使用类还是结构来保存数据取决于与特定数据相关的具体需求。对于保存大量信息的数据(例如客户对象),通常会使用引用类型,因为这类对象通常不会经常被实例化,而是会在程序的持续时间内长期 “存活”。而对于在变量间创建、操作和复制次数较多,特别是在紧密循环中的较小对象(例如表示复数的对象,由实部和虚部组成,可用于大量计算),则更适合使用结构,因为这通常会加快执行速度,减少内存消耗和垃圾回收器活动。无论如何,在使用值类型和引用类型时,仔细考虑它们在语义上的差异是非常重要的。