The Collection Classes

The class library in Dynamics AX contains a useful set of collection classes. A collection class can contain instances of any valid X++ type, including objects. The collection classes, SetListMapArray, and Struct, are sometimes referred to as foundation classes or, in earlier versions of the product, Axapta Foundation Classes (AFC).
All collection classes are kept in memory, so pay attention to their size when you insert instances in them. If you need to handle huge amounts of data, you should consider alternatives such as temporary tables or partly on-disk X++ arrays.
The elements inserted in the collections can be retrieved by traversing the collection class or by performing a lookup in it. To decide which collection class to use, you must consider your data and how you want to retrieve it. The following sections explain each collection class.

The Set Class

Set object is a collection that may hold any number of distinct values of any given X++ type. All values in the Set must have the same type. An added value that is already stored in the Set is ignored and does not increase the number of elements in the Set. The elements are stored in a way that facilitates looking up the elements. The following example illustrates this by creating a Set object with integers and adding 100, 200, and 200 (again) to the set.

Set set = new Set(Types::Integer);
;
set.add(100);
set.add(200);
set.add(100);
print set.toString(); //{100, 200}
print set.elements(); //2
print set.in(100);    //true
print set.in(150);    //false
pause;



A set is particularly useful in situations in which you want to sort elements as the elements in the set are sorted when inserted, or when you want to track objects. Here is an example from the AxInternalBase class.

protected boolean isMethodExecuted(str _methodName
, ...)
{
    if (setMethodsCalled.in(_methodName))
        return true;

    setMethodsCalled.add(_methodName);
    ...
    return false;
}



The setMethodsCalled object keeps track of which methods have been executed.
As Figure 15-2 shows, you can perform logical operations on a Set. You can create a union of two sets, find the difference between two sets, or find the intersection between two sets.

Figure 15-2. Set operations.


The logical operations are illustrated programmatically here.

Set set1 = new Set(Types::String);
Set set2 = new Set(Types::String);
;
set1.add('a');
set1.add('b');
set1.add('c');

set2.add('c');
set2.add('d');
set2.add('e');

print Set::union(set1, set2).toString();        //
 {a, b, c, d, e}
print Set::intersection(set1, set2).toString(); // {c}
print Set::difference(set1, set2).toString();   //
 {a, b}
print Set::difference(set2, set1).toString();   //
 {d, e}
pause;



The List Class

List objects are structures that may contain any number of elements that are accessed sequentially. A List may contain values of any X++ type. All the values in the List must be of the type defined when creating the List. Elements may be added at either end of the List. A List is similar to a Set, except a List can contain the same element several times, and elements in a List are kept in the order in which they were inserted. Here is an example that shows insertion of integers into a list of integers. Note that the last integer values are inserted at beginning of the list.

List list = new List(Types::Integer);
;
list.addEnd(100);
list.addEnd(200);
list.addEnd(100);
list.addStart(300);

print list.toString(); // 300, 100, 200, 100
print list.elements(); // 4
pause;



The Map Class

Map objects associate one key value with another value. Figure 15-3 illustrates this.

Figure 15-3. An example of Map.


You can use any type as the key and value, including class and record types. The key and the value do not have to be of the same type. Lookups in a Map are efficient, which makes Map objects useful for caching of information.
Multiple keys may map to the same value, but one key can map to only one value at a time. Adding a key and value pair to a place where the key is already associated with a value changes the association so that the key maps to the new value.
The following example shows how to populate a Map with the keys and values shown in Figure 15-3 and subsequently perform a lookup.

Map map = new Map(Types::String, Types::Enum);
Word wordType;
;
map.insert("Car", Word::Noun);
map.insert("Bike", Word::Noun);
map.insert("Walk", Word::Verb);
map.insert("Nice", Word::Adjective);

print map.elements(); //4;

wordType = map.lookup("Car");
print strfmt("Car is a %1", wordType); //Car is a Noun
pause;



Map throws an exception if lookup is called for a non-existing key. You can call exists to verify that a key exists before calling lookup. This is particularly useful inside transactions, where you cannot catch the exception gracefully. Here is an example.

if (map.exists("Car"))
    wordType = map.lookup("Car");



The Array Class

An Array object may hold instances of any one given type, including objects and records (unlike the arrays built into the X++ language). The values are stored sequentially. An Array can expand as needed, so you do not have to specify its size at the time of instantiation. As with arrays in X++, the indexing of Array objects is one-based (that is, counting of elements begins with one, not zero).

Array array = new Array(types::class);

array.value(1, new Point(1, 1));
array.value(2, new Point(10, 10));
array.value(4, new Point(20, 20));

print array.lastIndex();          //4
print array.value(2).toString();  //(10, 10)
pause;



The Point class is declared in a later example, in the section on serialization.

The Struct Class

Struct objects may hold a variety of values of any X++ type. A Struct collects information about a specific entity. For example, you can store information such as inventory item identifier, name, and price and treat this compound information as one instance.
Struct objects allow you to store information in much the same way that you do with classes and tables. You can think of a Struct as a lightweight class. A Struct object exists only in the scope of the code in which it is instantiated. It does not provide polymorphism like most classes, or persistence like tables. The main benefits of using a Struct are that you can dynamically add new elements and you are not required to create a new type definition in the AOT.
As shown in the following example, accessing elements in a Struct is not strongly typed because you reference Struct objects by using a literal string. You should use a Struct only when absolutely necessary. The Struct was introduced as a collection class to communicate with the property sheet API, described in Chapter 3, "The MorphX Designers."
Here is an example of how to use a Struct.

Struct item = new Struct("int Id; str Name");
;
item.value("Id", 1000);
item.value("Name", "Bike");

print item.toString();   //id=1000; Name="Bike"

item.add("Price", 299);
print item.toString();   //id=1000; Name="Bike";
 Price=299
print item.fields();     //3
print item.fieldName(1); //Id
print item.fieldType(1); //int
print item.value("Id");  //1000
pause;



Performance is an interesting topic related to the use of classes, tables, and the Struct class. Suppose you needed a composite type to store values. For this discussion, the composite type is a point composed of two real values: x and y. You could model this in three ways:
  • By using a Struct with two fields, x and y.
  • By defining a new class, in which the constructor takes x and y as parameters, and using two access methods to retrieve the values.
  • By defining a table with two fields, x and y. You do not have to insert records into the physical (or temporary) table; you use the record only to store the point in memory.
You could benchmark these three implementations by creating 5,000 instances of points, adding these to a Set, traversing the Set, and accessing all the point values. Figure 15-4 shows the remarkable result.

Figure 15-4. Performance of Struct objects, classes, and tables as composite types.


The first two implementations are comparable, but the third is five to seven times faster. The difference in performance is a result of the overhead in instantiation of objects and the number of method calls. A method call in X++ has a small overhead, which, in scenarios involving the database, is negligible. However, in this case, instantiation and method calls are the slowest operations performed.
The performance difference between the Struct and class implementations is simply a result of the difference in the number of method calls. In the Struct implementation, you must instantiate the Struct and call the valuemethod for both x and y. For the class implementation, you can instantiate and set the values through the constructor in one method call. For the table implementation, you can set the field values directly without a single method call, and also without instantiating an object. The following code was used to measure the performance.

//Struct implementation
for (i=1; i<=5000; i++)
{
    pointStruct = new struct("real x; real y");
    pointStruct.value("x", i);
    pointStruct.value("y", i);

    set.add(pointStruct);
}

//Class implementation
for (i=1; i<=5000; i++)
{
    pointClass = new Point(i, i);

    set.add(pointClass);
}

//Table implementation
for (i=1; i<=5000; i++)
{
    pointTable.x = i;
    pointTable.y = i;

    set.add(pointTable);
}



When accessing the values, the struct and class implementations perform poorly because a method call is required; the table implementation is much faster.
Note

When you insert a table type into a collection class, a memory copy operation is performed. Although table types in X++ are reference types, they behave as value types when inserted in collection classes.

If your implementation calls for fast in-memory storage and retrieval of composite types, you should favor using a table implementation. The tree view provided by the Permissions tab in Administration\User Permissions is built with the table approach. This implementation generates a deep and complex tree structure in a matter of seconds. Earlier versions of Dynamics AX applied the class approach, and in those versions it took significantly longer to build a much simpler tree. For more information about improving performance, see Chapter 17, "Performance."

Traversal

You can traverse your collections by using either an enumerator or an iterator. When the collection classes were first introduced in Dynamics AX, the iterator was the only option. But because of a few obscure drawbacks that appear as hard-to-find errors, enumerators were added, and iterators were kept for backward compatibility. To highlight the subtle differences, the following code shows how to traverse a collection with both approaches.

List list = new List(Types::Integer);
ListIterator iterator;
ListEnumerator enumerator;
;
//Populate list.
...

//Traverse using an iterator.
iterator = new ListIterator(list);
while (iterator.more())
{
    print iterator.value();
    iterator.next();
}

//Traverse using an enumerator.
enumerator = list.getEnumerator();
while (enumerator.moveNext())
{
    print enumerator.current();
}



The first difference is the way in which the iterator and enumerator instances are created. For the iterator, you call new, and for the enumerator, you get an instance from the collection class by calling the getEnumeratormethod. In most cases, both approaches will work equally well. However, when the collection class resides on the opposite tier from the tier on which it is traversed, the situation is quite different. For example, if the collection resides on the client tier and is traversed on the server tier, the iterator approach fails because the iterator does not support cross-tier referencing. The enumerator does not support cross-tier referencing either, but it doesn't have to because it is instantiated on the same tier as the collection class. Traversing on the server tier using the client tier enumerator is quite network intensive, but the result is logically correct. Because some code is marked as Called From, meaning that it can run on either tier, depending on where it is called from, you could have broken logic if you use iterators, even if you test one execution path. In many cases, hard-to-track bugs such as this surface only when an operation is executed in batch mode.
Note

In earlier versions of Dynamics AX, this problem was even more pronounced because development and testing sometimes took place in two-tier environments, and this issue surfaces only in three-tier environments.

The second difference between iterators and enumerators is the way in which the traversing pointer moves forward. In the iterator approach, you must explicitly call both more and next; in the enumerator approach, themoveNext method handles these needs. Most developers have inadvertently implemented an endless loop at least once, simply because they forgot to move a pointer. This is not a significant problem, but it does cause an annoying interruption during the development phase.
If you always use the enumerator, you will not encounter either of the preceding issues. The only situation in which you cannot avoid using the iterator is when you must remove elements from a List collection. The following code shows how this is accomplished.

List list = new List(Types::Integer);
ListIterator iterator;
;
list.addEnd(100);
list.addEnd(200);
list.addEnd(300);

iterator = new ListIterator(list);
while (iterator.more())
{
    if (iterator.value() == 200)
        iterator.delete();
    iterator.next();
}
print list.toString(); //{100, 300}
pause;



Serialization

Serialization is the operation of converting an object to a bit stream of data that is easily persisted or transported over the network. Deserialization is the opposite operation, in which an object is created from a bit stream. Serializing an object into a stream and later deserializing the stream into a new object must create an object whose member variables are identical to the original object.
Note

The Application frameworks RunBase and SysLastValue rely heavily on serialization. Classes in these frameworks implement the SysPackable interface, which requires implementation of pack and unpack methods.

All collection classes support serialization. The bit stream generated is in the form of a container, which is a value type. This is particularly useful when you are collecting information on one tier and want to transfer it to the opposite tier.
The following code shows a typical example, in which several records are placed in a map on the server tier and are consumed on the client tier. The benefit of using this approach (rather than simply returning a reference to the map object on the server tier) is the reduced number of client/server calls. The following implementation contains only one client/server call, calling the generateMap OnServer method. If the reference approach were used, each call to the enumerator would also be a client/server call, typically resulting in at least two client/server calls per element in the map. Here is the implementation using serialization.

client class MyClass
{
    private static server container
 generateMapOnServer()
    {
        Map map = new Map(typeId2Type(typeId
(RecId)), Types::Record);
        // Populate map.
        ...
        // Serialize the map.
        return map.pack();
    }
    public void consumeMap()
    {
        // Deserialize the map.
        Map map = Map::create(MyClass:
:generateMapOnServer());
        mapEnumerator enumerator = map
.getEnumerator();

        //Traverse map.
        while (enumerator.moveNext())
        {
            ...
        }
    }
}



In the preceding example, the Map object contains types, which are easy to serialize. The collection class is capable of serializing primitive X++ types and records. If a collection contains classes, the classes must provide an implementation of a pack method and a create method for the collection to be serializable. Here is an implementation of a serializable Point class.

class Point
{
    real x;
    real y;

    public void new(real _x, real _y)
    {;
        x = _x;
        y = _y;
    }

    public container pack()
    {
        return [x, y];
    }

    public static Point create(container _data)
    {
        real x;
        real y;
        [x, y] = _data;
        return new Point(x, y);
    }

    public str toString()
    {
        return strfmt('(%1, %2)', x, y);
    }
}



The following example is just one way of modeling a line by using a Set of Point classes. Notice how a new line instance is created by serializing and deserializing the line object.

Set line = new Set(Types::Class);
Set newLine;
;
line.add(new Point(0, 0));
line.add(new Point(2, 5));

print line.toString();     // {(0, 0), (2, 5)}

//Create a new instance.
newLine = Set::create(line.pack());
print newLine.toString();  // {(0, 0), (2, 5)}
pause;



Bringing It All Together

You have seen how collection classes allow you to collect instances of objects and values. The collection classes provide conceptually simple structures. The classes SetListMapArray, and Struct are easy to understand and just as easy to use. If you do a cross-reference to find all the places in the existing code where they are used, their usefulness is evident.
Sometimes, however, these collection classes are too simple to meet certain requirements. Suppose you needed to model a shape. In this case, having a List of points would be useful. Points can be modeled as a Structbecause the collection classes can contain objects, and an instance of a collection class is an object. You can combine collections classes to create, for example, a list of maps, a set of lists, or a set of lists of maps.
The SysGlobalCache class, described earlier in this chapter, is a good example of combining collection classes. It uses a map of maps of a given type. An example of a global cache instance is illustrated in Figure 15-5.

Figure 15-5. An example of internal structure in SysGlobalCache.


The values in the first Map are always strings (type str); this string is referred to as the owner of the entry in the cache. Each of these values maps to another instance of Map in which the types of the key and value are determined by the consumer of the cache. This way, the cache can be used to store instances of several types.
The values in the SysGlobalCache example shown in Figure 15-5 could be inserted by this code.

globalCache.set(classStr(MyClass), 1, object1);
globalCache.set(classStr(MyClass), 2, object2);
globalCache.set(classStr(MyOtherClass), "Value1",
 myIntegerSet);



Now examine how this is implemented inside the SysGlobalCache class, shown in the next code sample. The class has only one member variable, maps, which is instantiated in the new method as a mapping of strings to classes. The first time a value of type value is inserted in the cache, using the set method, a new instance of Map, named map, is created. The owner string is mapped to this map object in the maps member variable. Themap object maps values of the key type to values of the value type. The types are determined by using the typeOf function. The key and value pair is then inserted in map. The get method is implemented to retrieve values from the cache. To retrieve the values, the following two lookups must be performed:
  • A lookup in the owner-to-map Map to get the key-to-value Map.
  • A lookup in the key-to-value Map using the key to find the value.
If either lookup fails, the default return value specified as a parameter is returned.

class SysGlobalCache
{
    Map maps;

    private void new()
    {
        maps = new Map(Types::String, Types::Class);
    }

    public boolean set(str owner, anytype key,
 anytype value)
    {
        Map map;
        if (maps.exists(owner))
        {
            map = maps.lookup(owner);
        }
        else
        {
            map = new Map(typeOf(key), typeOf(value));
            maps.insert(owner, map);
        }
        return map.insert(key, value);
    }

    public anytype get(str owner, anytype key,
 anyType returnValue = '')
    {
        Map map;
        if (maps.exists(owner))
        {
           map = maps.lookup(owner);
           if (map.exists(key))
               return map.lookup(key);
        }
        return returnValue;
    }
    ...
}



Other Collection Classes

A few other collection classes are worth mentioning. They do not share the same structure as the collection classes explained so far, but you can use them for collecting instances.
The Stack Class
A stack is a structure in which you can add and remove instances from the top. This kind of structure, resembling a stack of plates, is sometimes described as a last in, first out (LIFO) structure. You add an instance to the top by calling push, and you remove the top instance by calling pop. The Stack class in Dynamics AX can hold only instances of containers. Because containers can contain any value type, you can still create a stack of integers, strings, dates, and so on.
Here is an example of how to use Stack.

Stack stack = new Stack();
;
stack.push([123]);
stack.push(["My string"]);

print conpeek(stack.pop(), 1); //My string
print conpeek(stack.pop(), 1); //123
pause;



The StackBase Class
Because the Stack class is limited to holding container instances, an improved stack was implemented called StackBase. The StackBase class provides the same functionality as the Stack class, except that it can hold instances of any given type.
Here is an example of how to use StackBase.

StackBase stack = new StackBase(Types::Class);
;
stack.push(new Point(10, 10));
stack.push(new Struct("int age;"));
print stack.pop().toString(); //(age:0);
print stack.pop().toString(); //(10, 10)
pause;



The RecordSortedList Class
If you have a list of records that you must either sort or pass as a parameter, you can use the RecordSortedList class. This collection class can hold only record types. When you insert a record in the list, it is sorted according to one or more fields that you specify. Because sorting takes place in memory, you can specify any fields, rather than just those for which a table index already exists. The combined sorting fields must constitute a unique key. If you need to sort by a non-unique field, you can add the RecId field, which is guaranteed to be unique, as a sorting field.
Here is an example in which customers are sorted by city by using RecordSortedList.

RecordSortedList list = new RecordSortedList
(tableNum(CustTable));
CustTable customer;
boolean more;
;
//Sort by City, RecId.
list.sortOrder(fieldNum(CustTable, City), fieldNum
(CustTable, RecId));

//Insert customers in the list.
while select customer
{
    list.ins(customer);
}

//Traverse the list.
more = list.first(customer);
while (more)
{
    info(strfmt("%1, %2", customer.Name, customer
.City));
    more = list.next(customer);
}

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.