Jaa


Changes to the Collection Classes in Dynamics AX 2012

In Microsoft Dynamics AX 2012 we introduced the ability to compile X++ code into .NET Framework CIL. CIL can run faster than interpreted X++ for computationally intensive applications, and for applications which create large numbers of objects that must be garbage collected. The mechanisms for running X++ as CIL include:

  1. Services
  2. runAs function (or Global::runClassMethodAsIL)
  3. Batch jobs

We intend for X++ code to produce the same results whether it is interpreted or run as CIL. We therefore had to make some minor changes to legacy X++ behavior. This article explains the change to the legacy behavior of the X++ interpreter in the area of the AX collection classes, which include the Map, Set, List, and Array classes.

1. Type Checking to Ensure Match

The first X++ interpreter change is an increase in the type matching checks for the AX collection classes.  In most cases, the value inserted into a collection should have the same or compatible type as the defined type of the collection classes.

For example, suppose we define a list with type being string, but we try to insert a different type:

    List l = new List(Types::String);

    l.addFront(1);

 Here an exception will be raised and the following text will be shown in the info log:

The expected type was str, but the encountered type was int.

We introduced this new restriction in X++ because type mismatches are not allowed in CIL.

NOTE: Due to legacy code issues of type mismatching in Maps, this check has been temporarily disabled for Maps in the X++ interpreter. This type check is enabled for the other collections (namely Set, List, and Array).

2. Disallow Null Values

The second change is that null values are no longer allowed as elements in Sets, or as keys in Maps.  Sets and Maps in .NET Framework have this same restriction.  For example:

static void Job1(Args _args)
{
    Map m;
    Set s;
    Query q;

    m = new Map(Types::Class,Types::String);
    m.insert(q, "abc"); //q is null so an exception is raised.

    s = new Set(Types::Class);
    s.add(q); //q is null so an exception is raised.
}

3. Invalidate Enumerators/Iterators After Changing Elements

The third change is to invalidate enumerators and iterators of a Map or Set object if any element of the collection is added or removed after the enumerator/iterator is created.  Consider the following code, where a map is initialized with a few elements:

static void Job2(Args _args)
{
    Map m;
    MapIterator it;
    str s;
 
    m = new Map(types::Integer,Types::String);
    it = new MapIterator(m); //The map iterator is constructed.

    m.insert(1, "abc");
    m.insert(2, "def");
    m.insert(4, "ghi");
    m.remove(2); //Contents of map are modified.

it.begin(); //This access of the iterator raises an exception.
}

An exception will be raised with the following message:

The iterator does not designate a valid element.

A map can be modified by MapIterator.delete() as well, and this will also cause the iterator to be invalid after the delete().   For example:

static void Job3(Args _args)
{
    Map m;
    MapIterator it;
 
    m = new Map(types::Integer,Types::String);
    it = new MapIterator(m); //The map iterator is constructed.
   m.insert(1, "abc");
    m.insert(2, "def");
    m.insert(4, "ghi");
   it.begin();
info(it.toString());
   it.delete(); //An element is deleted from the map.
it.next(); //The iterator is no longer valid, so an exception is raised.
info(it.toString());
}

The result is shown in the Infolog as below:

How shall we handle legacy X++ code that removes elements from a Map using MapIterator?

One option is to iterate through the Map elements, and copy the key of each unwanted element into a Set.  Next iterate through the Set, and for element, delete its match from the Map.  For example:

static void Job4(Args _args)
{
    Map m;
    MapIterator it;
    Set s;
   SetEnumerator sm;
 
   m = new Map(types::Integer,Types::String);  //Keys are of type Integer.
   m.insert(1, "abc");
    m.insert(2, "def");
    m.insert(4, "ghi");
   it = new MapIterator(m);
   s = new Set(Types::Integer); //This Set stores the type that matches the Map keys.

   it.begin();
   while(it.more()) //Iterate through the Map keys.
   {
       if(it.domainValue() mod 2 == 0)
           s.add(it.domainValue()); //Copy this unwanted key into the Set.
       it.next();
   }
  
   sm = s.getEnumerator();
   while(sm.moveNext()) //Iterate through the Set.
   {
       m.remove(sm.current()); //Delete the key from the Map.
   }
}

Remaining Inconsistencies

Currently there remain two inconsistencies in AX collection behavior between (a) X++ by the interpreter versus (b) X++ as CIL. One involves deletions, the other insertions:

  1. Deletions:  Deleting from a Set by using SetIterator.delete() works fine when running X++ by the interpreter. Yet in X++ as CIL this deletion raises an exception saying the iterator is invalid.  We will fix this inconsistency in the near future.
  2. Insertions:  Inserting an item into a Map or Set that is currently being used by an enumerator or iterator works fine when running X++ by the interpreter. Yet in X++ as CIL the enumerator or iterator becomes invalid after the insertion and it raises an exception. 

For #2 Insertions into a Map, the inconsistency is illustrated in the following example:

static void Job5(Args _args)
{
    Map m = new Map(types::String,Types::Integer);
    MapEnumerator me = m.GetEnumerator();
 
   m.insert("1", 10);
    info(m.toString());

   me.moveNext(); //This line causes different behavior between X++ interpreter versus CIL.
   info(me.toString());
}

This job run in the interpreter gives the following fine results in the Infolog:

However, if we run the job as CIL, then an exception is raised:

System.InvalidOperationException: Collection was modified after the enumerator was instantiated.

This inconsistency exist now because the amount of legacy X++ code that could break when runing in X++ interpreter mode.  Maps are often updated using the pattern in the following example:

static void Job6(Args _args){   Map m;
    MapEnumerator me;
   str newValue;

   m = new Map(types::Integer,Types::String);    m.insert(1, "abc");
    m.insert(2, "def");
    m.insert(4, "ghi");
   me = m.getEnumerator();
    while(me.moveNext())   {       newValue = me.currentValue() + "aaa";       iim.insert(me.currentKey(), newValue); //The map is updated by an insert, all works.    }}

Comments

  • Anonymous
    November 26, 2014
    very usefull good thnax