Search This Blog

Tuesday, September 13, 2011

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.
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 post 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-
Define a list with type being string, but 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.
Ax2012 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 krishh_NullValidation(Args _args)
{
 
   Map m;
 
   Set s;
 
   Query q;

     ;
 
   m = new Map(Types::Class,Types::String);
 
   m.insert(q, "abc");  //q is null which is not instantiated 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());


 
}

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 successfully in the interpreter gives results.

However, if we run the above job as CIL, then an exception is raised:
System.InvalidOperationException: Collection was modified after the enumerator was instantiated.
}
 

No comments:

Post a Comment

Thanks for visiting my blog,
I will reply for your comment within 48 hours.

Thanks,
krishna.