In .Net, there are two kind of types that hold data, reference types (or classes) and value types (or structures). The two of them have different semantics in the way they are used, but both can contain themselves other reference and/or value types.
A class (or a reference type), is the most common way of storing data. Its name comes from the fact that a variable of a reference type (commonly referred to as an instance of the type) does not contain the data directly, but instead points to (references) a memory location where the actual data is stored. A class in X# is defined through the CLASS...END CLASS statement and can INHERIT from another reference type, implement one or more INTERFACES and may contain fields, properties, constructors, methods, events and other items:
CLASS Customer
EXPORT name AS INT // exported (public) field
PROTECT age AS INT // protected field, not visible to code outside the class
END CLASS
Typically classes and all their members are defined in a single file of code. If it's necessary for class members to be defined in multiple files (for example when the amount of class members is very big), then the class must be defined in every file as PARTIAL:
PARTIAL CLASS Customer
// class members
END CLASS
Since an instance of a class is only storing a pointer to the data, one or more variables can point to the exact same object in memory. Assigning a variable of a reference type to another variable of the same type results to both representing the same data. Updating data using one reference automatically updates the other reference as well:
FUNCTION Start() AS VOID
LOCAL one, two AS Customer
one := Customer{}
two := one // now both vars point to the same object in memory
two:name := "Robert"
? one:name // also "Robert"
A class can even contain other types (classes or structures). In this case the type inside the main type is called a nested type. Nested types can be used by using the name of their container class and their own name, connected by a dot:
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
Nested classes are useful in particular for defining helper classes, a class that is only used in the context of the parent class, for holding information only relevant to that class. Creating a nested class for this data, instead of using a regular class results to better structured code.
A structure (or a value type), in contrast to reference types, stores its data directly. It has some similarities to the Visual Objects STRUCTURE feature (renamed in X# to VOSTRUCT), but it is a lot more powerful than in VO, as it can contain most items that also reference types have, like properties, constructors, methods etc. Unlike reference types though, value types cannot inherit from other types or implement interfaces. They can contain nested classes or structures though. Value types can be defined with the STRUCTURE Statement:
STRUCTURE Vector2D
EXPORT x AS INT
EXPORT y AS INT
METHOD Invert() AS VOID
SELF:x := - SELF:x
SELF:y := - SELF:y
END STRUCTURE
Since structures hold their data directly, instantiating them does not involve any additional memory consumption than the memory needed for their data itself (reference types need memory for the data and also for the pointer to the data) or garbage collector activity. They are mostly suitable as light weight data containers, usually hold a small amount of fields, typically 2-4, but can even contain a single element, like the System.Int32 (INT) or System.Boolean (LOGIC) data types which simply define the INT and LOGIC data types, including several methods for manipulating their data. Other very commonly used system defined structures are System.Drawing.Point, System.Drawing.Rectangle etc, all containing a small amount of data fields.
Structures also have different semantics when using them compared to regular classes. It is not necessary to instantiate such a variable to use it, since declaring a var of a value type results to its data being allocated directly:
FUNCTION Start() AS VOID
LOCAL vector AS Vector2D
vector:x := 10
vector:y := 20
Although, for convenience, it's possible to also define constructors in value types and instantiate them as with regular classes:
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
The most important difference that must always be taken under consideration, is that assigning a value type to another one results to the data of the source being copied to the destination, so unlike what happens with reference types, the data of the two variables are stored in separate memory locations and any changes to one variable will not affect the other:
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, value was copied from first vector
vec_2:x := 40 // put a new value to second vector
? vec_1:x // 10 again, first vector value still has its original value
For this reason structures are not suitable for very large objects, since assigning one to another or passing one as an argument to a method involves copying all data from the source to the destination. On the other hand, with regular classes, only the pointer to the data is passed as an argument to a method.
Another important difference between reference and value types, is the behavior of the equals operator (==). For reference types, the equals operator between two variables only compares the pointers themselves, not the data of the objects. So it returns TRUE only when both variables point to the same object and in all other cases it returns FALSE, even if the data both objects contain is the same:
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, because o1 and o2 point to different memory locations
o2 := o1
? o1 == o2 // TRUE
On the other hand, by default the == operator cannot be used on structures and the compiler will report an error if you try to do so. It can be made possible to use it though, by defining an OPERATOR method in the structure that implements how the comparison with == should be done. In the sample below, the == operator is implemented to compare the actual data that the two compared structures hold, so that it returns TRUE, when the data is equal:
STRUCTURE ValueType
EXPORT data AS STRING
OPERATOR == (a AS ValueType, b AS ValueType) AS LOGIC
RETURN a:data == b:data // let the equals == operator return true when the data of the two arguments is the same
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
Note that it is possible to compare values of most common system defined structures like System.Int32, System.Boolean, System.Double, because they also have defined equals operator methods like the one in the code above.
Weather to use a class or a structure for holding data depends on the specific needs related to the particular data. For data holding a lot of information (for example a customer object) you would typically use a reference type, as such objects usually don't get instantiated very often, but usually "live" long for the duration of the program. For smaller objects, that are being a created, manipulated and copied between variables a lot of times and in particular in tight loops (like for example an object representing a Complex number, consisting of a real and an imaginary part, which can be used in a lot of calculations), it is' more suitable to use a structure, as this will typically lead to faster execution, with a lot less memory consumption and garbage collector activity. In any case, it's very important to carefully consider their differences in semantics when using value vs reference types.