Have you ever wished you could make a class that provides functionality but is ignorant of the type of the data it contains – for example, a list that contains data of one type, except the type is not known when the class is implemented? Generic classes give you this ability.
In this article, I provide a simple introduction to generic classes. This is only the tip of the generic iceberg, however; more advanced topics, such as generic interfaces, constraints, etc., are not covered here.
Generic classes cannot be instantiated directly. Instead, they require one or more “type parameters,” which are used to create what is called a “constructed” type. A constructed type is a new type in which the types represented by the type parameters are substituted with the actual types that you use when declaring the instance of a generic type. In addition, generic types also verify that members being set or arguments being passed are compatible with the type parameters that were used to declare them when the type was constructed, thereby increasing type safety.
The .NET Framework offers many instances of generic classes for you to use. One of the most useful of these classes is System.Collections.Generic.List, which allows the creation of lists that contain any .NET types as elements. One benefit of generics is that the System.Collections.Generic.List class can be implemented without knowing the type of the element beforehand. If non-specific object handles were used instead, type compatibility of the list members could not be assured.
As an example, let’s create a System.Collections.Generic.List that contains elements of a type we’ll define. Consider the following program:
import System.Collections.Generic
namespace listElem
cls structure customer
firstName ,string
middleInit ,char
lastName ,string
number ,decimal
endstructure
endnamespace
record
custList ,@System.Collections.Generic.List<listElem.customer>
elem ,listElem.customer
proc
;Create the list
custList = new System.Collections.Generic.List<listElem.customer>()
;Set the data for the first element
elem.firstName = "Fred"
elem.middleInit = "X"
elem.lastName = "Flintstone"
elem.number = 1
;Add the element to the list
custList.Add(elem)
end
First we create a structure, and then from that structure, we can create a list of structures. Not only are we creating an object handle to this list, we are creating a constructed type with “customer” being the one type parameter required by the System.Collections.Generic.List class. The type parameters are listed, separated by commas if more than one is required, in angle brackets.
Then, in the procedure division, we can create a new instance of the constructed type and store it in the “custList” handle.
To create your own generic class, you declare the class in the usual way but add a type parameter list. You can then use the types in the list to substitute for types within the class. Let’s create a node for a binary tree:
namespace binTree
class treeNode<T>
iData ,T
iLeft ,@treeNode<T>
iRight ,@treeNode<T>
method treeNode
val ,T
proc
Data = val
end
method ~treeNode
proc
Left = ^NULL
Right = ^NULL
end
property Data ,T
method get
proc
mreturn iData
end
method set
proc
iData = value
end
endproperty
property Left ,@treeNode<T>
method get
proc
mreturn iLeft;
end
method set
proc
iLeft = value
end
endproperty
property Right ,@treeNode<T>
method get
proc
mreturn iRight;
end
method set
proc
iRight = value
end
endproperty
method AddLeft ,void
val ,T
proc
Left = new treeNode<T>(val)
end
method AddRight ,void
val ,T
proc
Right = new treeNode<T>(val)
end
endclass
endnamespace
Of course, this is a very simplistic tree and does not guard against severing sub-trees, keeping the tree balanced, or traversing the tree, but it serves as a useful example for generic classes.
Note that “T” is the type parameter for this generic class. Wherever “T” is referenced, the type passed when constructing an instance of that type (the constructed type) is used to define the member being declared. The data field is given that type, so the tree node contains the type specified when the constructed type is created. Also, the left and right object handles, which point to other instances of the type we are creating, need to specify the same type parameter list as the type we are declaring so that the type being pointed to is the same as the type being constructed.
The type parameter is also used as the parameter type for the constructor of the type and as the return type of the Data property that retrieves the value. The AddLeft and AddRight methods allow a node to be added to the tree without first constructing a new tree node.
In the following test program, two trees — one containing integers and one containing strings — are built. The generic tree class is used here with integer and string data, but it could be used with other types, including both value types and objects.
main
record
itree ,@treeNode<int>
ctree ,@treeNode<string>
proc
itree = new treeNode<int>(5) ;Create the tree's root node
itree.AddLeft(2) ;Add a left node
itree.AddRight(7) ;Add a right node
itree.Right.AddRight(10) ;Add a right node to it
ctree = new treeNode<string>("g") ;Create the tree's root node
ctree.AddLeft("c") ;Add a left node
ctree.AddRight("i") ;Add a right node
ctree.Left.AddLeft("b") ;Add a left node to it
stop
end
In the record, object handles are created that contain tree nodes of the constructed types for the root of a tree for each type (int and string). In each of the trees, a root node is created and left and right nodes are added. A right node is then added to the right node of the root of the integer tree, and a left node is added to the left node of the root of the string tree.
You can see how generics enable you to specify a class that provides common functionality regardless of the type of data being used. This along with other techniques can make it easier to build up libraries of reusable classes to help facilitate development. For more information, see the “Generic Classes” section in chapter 8 of the Synergy Language Reference Manual.