пятница, 10 августа 2007 г.

15. System Classes \ The Collection Classes

(последнее обновление: 10.08.07)
(основной перевод: 27.07.07 - 10.08.07)


Классы-коллекции

Библиотека классов в Dynamics AX содержит полезный набор классов-коллекций (collection classes). Класс-коллекция может содержать данные любого допустимого в X++ типа, включая объекты. Классы-коллекции - Set, List, Map, Array и Struct - иногда называют фундаментальными классами (foundation classes) или, как это было в предыдущих версиях программы, Axapta Foundation Classes (AFC).

Все классы-коллекции хранятся в памяти, поэтому при добавлении в них новых элементов следует помнить об увеличении размеров коллекции в памяти. Если же вам необходимо обрабатывать огромные объемы данных, то следует рассмотреть альтернативы, такие как временные таблицы или частично размещенные на диске массивы X++.

Доступ к элементам коллекции может быть получен путем последовательного перебора всей коллекции (traversing) или при помощи направленного поиска в ней (lookup). Чтобы решить, какие классы-коллекции использовать в конкретном случае, вы должны проанализировать ваши данные и подумать, как именно вы хотите получать доступ к элементам коллекции. В следующих разделах подробно рассматривается каждый из классов-коллекций.

Класс Set ("Множество", "Набор")

Объект Set представляет собой коллекцию, которая может содержать любое число отличающихся друг от друга (уникальных) значений любого имеющегося в X++ типа данных. Все значения в Set должны быть одного и того же типа. Попытка добавления в Set значения, которое уже там имеется, игнорируется и не приводит к увеличению количества элементов. Элементы в коллекции хранятся способом, облегчающим их поиск. В следующем примере демонстрируется создание объекта Set, состоящего из целочисленных значений, и последующее добавление в него значений 100, 200 и еще раз 100:
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;
Set особенно полезно в ситуациях, когда вы хотите сортировать элементы, так как элементы в нем сортируются при вставке, либо когда вы хотите отслеживать объекты. Вот пример из класса AxInternalBase:

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

setMethodsCalled.add(_methodName);
...
return false;
}
Объект setMethodsCalled отслеживает, какие методы были выполнены.

Как показано на рисунке 15-2, вы можете выполнять логические операции с ипользованием Set. Вы можете создать объединение (union) двух множеств, найти пересечение (intersection) между ними или найти отличие (difference) одного от другого.

Рисунок 15-2. Операции Set.


Логические операции можно проиллюстрировать следующим фрагментом кода:
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;

Класс List ("Список")

Объекты класса List представляют собой структуры, которые могут содержать любое количество элементов, доступ к которым осуществляется последовательно. List может содержать значения любого типа X++. Все значения в List должны соответствовать этому типу, указываемому при создании списка. Элементы могут быть добавлены на один из "краев" списка - либо в конец, либо в начало. List аналогичен Set, за исключением того, что List может содержать повторяющиеся значения, и порядок хранения элементов в списке определяется последовательностью операторов вставки значений в список. Следующий пример показывает добавление целых чисел в список (заметьте, что последнее число 300 вставляется в начало списка):
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;

Класс Map ("Карта соответствия")

Объекты данного типа устанавливают соответствие между одним значением ("ключом") и некоторым другим значением ("значением"). Рисунок 15-3 иллюстрирует это:

Рисунок 15-3. Пример Map.


В качестве ключа (key) и значения (value) можно использовать данные любого типа, включая класс и запись. Ключ и значение не обязаны быть одного и того же типа. Возможность эффективного поиска в Map делает объекты этого класса полезными для кэширования информации.

Несколько разных ключей могут указывать одновременно на одно и то же значение, тогда как один ключ одновременно может относиться только к одному значению. Добавление пары "ключ-значение" в Map, где такой ключ уже имеется и связан с некоторым иным (старым) значением, фактически приводит к замене старого значения на новое.

В следующем примере показано, как заполнить Map ключами и значениями, приведенными на рисунке 15-3, а затем выполнить поиск:
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 генерирует исключение, если метод lookup вызывается для поиска несуществующего ключа. Вы можете вызвать метод exists для проверки существования ключа перед вызовом lookup. Это особенно полезно в транзакциях, где вы не в состоянии элегантно перехватить исключение:
if (map.exists("Car"))
wordType = map.lookup("Car");

Класс Array ("Массив")

Объект Array может содержать элементы одного заданного типа данных, в том числе объекты и записи (в отличие от массивов, встроенных в язык X++). Значения хранятся последовательно. Массив может расширяться по мере необходимости, так что вам не нужно задавать его размер в момент инициализации. Как и для встроенных массивов X++, подсчет элементов в Array начинается с 1 (one-based indexing), а не с 0.
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;
Используемый в примере выше класс Point объявляется ниже - в примере раздела, посвященного сериализации.

Класс Struct ("Структура")

Объекты Struct могут содержать наборы значений любого типа X++. В Struct хранится информация о некоторой сущности. Например, вы можете хранить такую информацию о складской номенклатуре, как идентификатор, наименование и цену, и обращаться с этим комплексом информации как с "единым целым".

Объекты Struct позволяют хранить информацию подобно классам и таблицам. Вы можете представить себе Struct как облегченный класс. Объект Struct существует только в области видимости программного кода, в котором он обрабатывается - что не обеспечивает полиморфизма, присущего большинству классов, или "сохраняемости", присущей таблицам. Основные преимущества использования Struct заключаются в том, что вы можете динамически добавлять новые элементы и вам не требуется определять новый тип данных в AOT.

Как показано в нижеследующем примере, доступ к элементам Struct не сильно типизирован - вы ссылаетесь на объекты Struct, используя строковый литерал. Вам следует использовать Struct только в случае крайней необходимости. Ранее Struct уже была представлена в примере как класс-коллекция для обеспечения доступа к API таблицы свойств (property sheet), описанной в главе 3 "The MorphX Designers".

Вот пример использования 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;
Производительность - интересная тема, связанная с использованием классов, таблиц и класса Struct. Предположим, вам нужен составной тип для хранения значений. Допустим, что этот составной тип - точка, характеризующаяся двумя вещественными значениями (координатами): x и y. Вы можете смоделировать эту точку тремя способами :

* Используя Struct с двумя полями, x и y.

* Определив новый класс, в котором конструктор принимает x и y как параметры, и используя два метода доступа к значениям координат.

* Определив таблицу с двумя полями, x и y. При этом вам не нужно будет вставлять записи в постоянную (или временную) таблицу; вы используете запись только для хранения точки в памяти.

Можно протестировать эти три способа, создав каждым из них 5000 точек, присвоив значения их координатам и добавив в общий набор (Set). Рисунок 15-4 показывает замечательный результат.

Рисунок 15-4. Эффективность объектов Struct, классов и таблиц как составных типов.


Два первых способа сопоставимы по скорости, но третий - в пять-семь раз быстрее. Разница в показателях объясняется издержками на инициализацию объектов и большим количеством вызовов методов. Эти издержки пренебрежимо малы в третьем случае.

Разница между Struct и классом является просто результатом различия в количестве вызовов методов. В случае Struct необходимо создать экземпляр Struct и вызвать метод value как для х, так и для y. В случае класса можно создать объект этого класса и присвоить значения с помощью конструктора, обойдясь одним вызовом метода. В случае таблицы можно задать значения полей непосредственно без единого вызова метода, а также без создания экземпляра объекта. Следующий код был использован для оценки результатов:
//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);
}
Struct и класс показывают слабую производительность при выполнении доступа к значениям, поскольку требуют вызова методов; табличный же вариант оказывается намного быстрее.

ПРИМЕЧАНИЕ
Когда вы вставляете значение табличного типа в класс-коллекцию, выполняется операция копирования этого значения в память. И хотя табличные типы в X++ представляют собой ссылки, они ведут себя как значения при вставке в классы-коллекции.

Если вам требуются хранящиеся в памяти, быстродоступные составные типы, то вам должен понравиться табличный подход. Дерево (tree view), представленное на вкладке Permissions в пункте меню Administration\User Permissions строится с использованием табличного подхода. Это обеспечивает генерирование дерева сложной структуры за считанные секунды. В предыдущих версиях Dynamics AX применялся подход с использованием класса, и в этих версиях значительно дольше строится дерево с гораздо более простой структурой. Дополнительную информацию о повышении производительности можно получить из главы 17 "Performance".

Перебор элементов в коллекции

Вы можете последовательно перебрать все элементы в коллекции, используя либо енумератор (enumerator, "перечислитель"), либо итератор (iterator, "повторитель"). Когда классы-коллекции впервые появились в Dynamics AX, итератор был единственно возможным вариантом. Но из-за некоторых вскрывшихся недостатков, которые проявляются в виде труднонаходимых ошибок, были добавлены енумераторы, а итераторы были сохранены для обратной совместимости. Чтобы подчеркнуть тонкие различия, следующий код показывает, как перебрать коллекцию с использованием обоих подходов:
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();
}
Первое различие заключается в том, каким образом создаются итератор и енумератор. Для итератора вы вызываете new, а енумератор вы получаете из класса-коллекции, вызыая метод getEnumerator. В большинстве случаев оба подхода будут работать одинаково хорошо. Однако, когда класс-коллекция находится на уровне, противоположном тому, на котором производится перебор элементов, ситуация совершенно иная. Например, если класс-коллекция находится на уровне клиента, а перебирается на уровне сервера, применение итератора становится невозможным, поскольку итератор не поддерживает межуровневые ссылки. Енумератор также не поддерживает межуровневые ссылки, но ему это и не требуется, поскольку он создается на том же уровне, что и класс-коллекция. Перебор коллекции на уровне сервера с использованием енумератора на уровне клиента вызывает довольно интенсивную нагрузку на сеть, но результат получается логически правильным. Поскольку некоторый код помечен как Called From, что означает, что он может работать на любом уровне, в зависимости от того, где он вызывается, вы могли бы нарушить логику, если бы вы использовали итераторы, даже если бы досконально оттестировали выполнение на одном уровне. Во многих случаях трудно отследить подобные ошибки, так как они проявляются только в том случае, если операция выполняется в пакетном режиме.

ПРИМЕЧАНИЕ
В предыдущих версиях Dynamics AX эта проблема была даже более явной, поскольку разработка и тестирование производились в двухуровневой среде, и проблема проявлялась только при переходе к трехуровневой среде.

Второе различие между итераторами и енумераторами проявляется в том, как указатель перебора коллекции (traversing pointer) продвигается вперед. В случае итератора вы должны явно вызывать пару методов: more и next; в случае енумератора - только один метод moveNext. Большинство разработчиков хотя бы раз невольно попадали в ситуацию с бесконечным циклом, потому что забывали перевести указатель. Это не является серьезной проблемой, но вызывает раздражающие заминки на этапе разработки.

Если вы всегда будете использовать енумератор, то не испытаете вышеописанных проблем. Единственная ситуация, в которой вы не сможете избежать использования итератора - удаление элементов из коллекции List. Следующий код показывает, как это делается:
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) есть операция преобразования объекта в двоичный поток данных, который легко сохраняется или транспортируются по сети. Десериализация (deserialization) есть обратная операция, в которой объект создается из потока битов. После сериализации объекта в поток и последующей десериализации потока в новый объект этот новый объект должен создаваться с переменными (member variables), идентичными переменным первоначального объекта.

ПРИМЕЧАНИЕ
Программные каркасы приложения (application frameworks) RunBase и SysLastValue прочно завязаны на сериализацию. Классы в этих каркасах реализуют интерфейс SysPackable, который требует реализации методов упаковки и распаковки: pack и unpack.

Все классы-коллекции поддерживают сериализацию. Двоичный поток генерируется в виде контейнера (тип данных - container). Это особенно полезно, когда вы хотите собрать информацию на одном уровне (звене) и перенести ее на противоположный уровень.

Ниже показан типичный пример кода, в котором несколько записей помещаются в карту соответствия (map) на сервере и затем используются на уровне клиента. Польза от использования именно такого подхода (а не просто возвращение ссылки на объект map на сервере) заключается в уменьшении числа вызовов между клиентом и сервером. В примере содержится всего лишь один такой вызов - вызов метода generateMapOnServer. Если бы использовался ссылочный подход, то каждый вызов енумератора так же был бы клиент/серверным вызовом, в результате чего на каждый элемент в map приходилось бы как минимум два вызова между клиентом и сервером. Вот реализация с использованием сериализации:
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())
{
...
}
}
}
В предыдущем примере объект Map содержит типы данных, которые легко сериализуются. Класс-коллекция способен сериализовать примитивные типы X++ и записи. Если же коллекция содержит классы, то эти классы должны обеспечивать реализацию методов pack и create для того, чтобы иметь возможность быть сериализованными. Вот реализация класса Point, способного к сериализации:
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);
}
}
Следующий пример - лишь один из способов моделирования линии с помощью набора (Set) классов Point. Обратите внимание, как новая линия (объект newLine) создается при помощи сериализации и десериализации объекта line:
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;


Совместное использование

Вы уже видели, как классы-коллекции позволяют вам наполнять их экземплярами объектов и значений. Классы-коллекции представляют собой концептуально простые структуры. Классы Set, List, Map, Array и Struct легки для понимания и столь же просты в использовании. Если вы, используя перекрестные ссылки, найдете все места в существующем коде системы, где они используются, то их полезность становится очевидной.

Иногда, однако, эти классы-коллекции оказываются слишком простыми, чтобы удовлетворять определенным требованиям. Предположим, вам необходимо моделировать геометрическую форму (shape). В этом случае, полезно будет иметь список точек (List of Points). Точки Points могут быть смоделированы как структуры (Struct), поскольку классы-коллекции могут содержать объекты, а экземпляр класса-коллекции также может быть объектом. Вы можете комбинировать классы-коллекции для того, чтобы создать, например, список карт (list of maps), набор списков (set of lists) или набор списков карт (set of lists of maps).

Класс SysGlobalCache, описанный ранее в этой главе, является хорошим примером комбинирования классов-коллекций. Он использует карту карт (map of maps) заданного типа. Пример экземпляра глобального кэша показан на рисунке 15-5.

Рисунок 15-5. Пример внутренней структуры в SysGlobalCache.


Значения в первой карте всегда являются строками (типа str); такая строка является ссылкой на владельца входа в кэше. Каждое из этих значений соответствует экземпляру другой карты, в которой типы ключа и значения определяются потребителем кэша. Таким образом, кэш может быть использован для хранения экземпляров различных типов.

Значения в примере c SysGlobalCache, показанном на рисунке 15-5, могут быть вставлены посредством следующего кода:
globalCache.set(classStr(MyClass), 1, object1);
globalCache.set(classStr(MyClass), 2, object2);
globalCache.set(classStr(MyOtherClass), "Value1", myIntegerSet);
Теперь изучим, как это осуществляется в классе SysGlobalCache, показанном в нижеследующем примере кода. Класс имеет только одну (member) переменную maps, которая создается в методе new как карта соответствия "строки - классы". В первый раз значение типа "значение" помещается в кэш при помощи метода set, в котором создается новый экземпляр карты с именем map. Строка "владелец" (owner) ставится в соответствие объекту map при помощи переменной maps. Объект map устанавливает соответствие между значением типа "ключ" и значением типа "значение". Типы определяются с помощью функции typeOf. Далее пара "ключ-значение" вставляется в карту. Метод get используется для получения значений из кэша. Чтобы получить значение, необходимо выполнить две следующие операции поиска (lookup):

* Поиск в карте "владелец-карта", чтобы получить карту "ключ-значение";

* Поиск в карте "ключ-значение" с использованием "ключа" для нахождения "значения".

Если поиск неудачен, то возвращается значение по умолчанию, указанное в качестве параметра.
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;
}
...
}

Другие классы-коллекции

Стоит упомянуть о нескольких других классах-коллекциях. Они имеют иную структуру, нежели классы-коллекции, рассмотренные выше, но вы можете также использовать их для "коллекционирования" элементов.

Класс Stack

Стек - это структура, в которой вы можете добавлять элементы в ее верхушку и удалять их из верхушки. Такого рода структуру, напоминающую стопку тарелок, иногда называют "последний пришел, первый ушел" ("last in, first out" = LIFO). Вы добавляете элемент в верхушку стека посредством вызова метода push и удаляете верхний элемент при помощи метода pop. Экземплярами коллекции в классе Stack могут быть только контейнеры (тип container). Поскольку контейнеры могут содержать значения любого типа (кроме объектов), вы все равно можете создать стек целых чисел, строк, дат и так далее.

Вот пример использования стека:
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;

Класс StackBase

Поскольку возможности стека ограничены использованием контейнерного типа, был разработан улучшенный стек, названный StackBase. Класс StackBase предоставляет те же функциональные возможности, что и класс Stack, за исключением того, что он может содержать элементы любого типа.

Вот пример использования класса 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;

Класс RecordSortedList

Если у вас есть список записей, который необходимо либо отсортировать, либо передать в качестве параметра, вы можете использовать класс RecordSortedList. Этот класс-коллекция может содержать только элементы с типом запись. Когда вы вставляете запись в список, то он сортируется по одному или нескольким полям, которые вы задаете. Поскольку сортировка происходит в памяти, вы можете указать любые поля, а не только те, для которых существует индекс в таблице. Комбинация полей сортировки должна быть уникальным ключом. Если вам нужно выполнить сортировку по неуникальному полю (полям), то к ним можно добавить поле RecId, которое гарантированно является уникальным.

В следующем примере клиенты сортируются по городу с использованием 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);
}

Комментариев нет: