Новый синтаксис ABAP. Часть 2
Продолжаем рассматривать изменения синтаксиса ABAP, появившиеся начиная с версии 7.4: инлайновые декларации, операторы-конструкторы, новые приёмы работы с таблицами, мэши, перечисления. Материал написан не как справочник, а как рассказ о взрослении языка.

4. Новости о внутренних таблицах
Приятные нововведения произошли в работе с внутренними таблицами.
4.1. Индексные и ключевые выражения
Давным-давно было модно называть внутреннюю таблицу и ее рабочую область одним и тем же именем. Для этого внутренняя таблица объявлялась, как внутренняя таблица с заголовком с опцией WITH HEADER LINE. Такая возможность есть и сейчас, но уже по счастью вышла из моды. Дело в том, что возможность называть одним и тем же именем два разных куска данных автоматически приводит к вероятности ошибки. Попробуйте передать имя это таблицы в параметр подпрограммы, метода или функционального модуля. Если параметр достаточно типизирован, ничего страшного не произойдет, просто на этапе компиляции появится сообщение об ошибке. Хуже, когда параметр не типизирован (для подпрограмм) или типизирован предельно широко (для методов или функциональных модулей). Тогда ошибка появится во время исполнения. Осознав такой интересный эффект от таблиц с заголовком, разработчики ABAP придумали возможность различать тело таблицы и ее заголовок. В тех случаях, когда по синтаксису языка может использоваться и тело и заголовок, по умолчанию используется заголовок. А к телу в таком случае можно обратиться, указав после имени открывающую и закрывающую квадратные скобки.
Но внутри этих квадратных скобок ничего писать нельзя до версии 7.4. Даже пробел нельзя писать. А вот начиная с версии 7.4 появилась возможность в этих квадратных скобках писать индексные и ключевые выражения.
В стандартных и сортированных внутренних таблицах (но не хэшированных) в квадратных скобках можно писать индексные выражения, то есть просто указывать номер строки. Или целочисленное выражение, имеющее корректное для данной внутренней таблицы значение (Лист. 17, результат — Рис. 15).
types tt_tactt type standard table of tactt with default key. DATA(gt) = VALUE tt_tactt( ( spras = 'E' aCtVT = ' @' ltext = ' Dog' ) ( spras = 'R' aCtVT = ' @' ltext = ' Собака' ) ). write: / 'Whole 1st line ', gt[ 1 ] color col_key , / 'Whole 2nd line ', gt[ 2 ] color col_key , / 'LTEXT in 1st line', gt[ 1 ]-ltext color col_key , / 'ACTVT in 2st line', gt[ 2 ]-actvt color col_key. gt[ 1 ]-ltext = 'HO-HO-HORSE'. data(i) = 3. skip. write: / 'Whole 1st line ', gt[ 1 ] color col_key , / 'Whole 2nd line ', gt[ 2 ] color col_key , / 'LTEXT in 1st line', gt[ 1 ]-ltext color col_key , / 'ACTVT in 2st line', gt[ i - 1 ]-actvt color col_key.
Лист. 17. Индексные выражения

Рис. 15. Результат Лист. 17
Так называемые ключевые выражения позволяют выбирать запись и в стандартных, и в сортированных, и в хэш-таблицах. Для этого в скобках указываются наборы значений полей (Лист. 18).
types tt_tactt type standard table of tactt with default key. *types tt_tactt type standard table of tactt with empty key. DATA(gt) = VALUE tt_tactt( ( spras = 'E' aCtVT = ' @' ltext = ' Dog' ) ( spras = 'R' aCtVT = ' @' ltext = ' Собака' ) ). write: / 'Whole 1st line ', gt[ spras = 'E' ] color col_key , / 'Whole 2nd line ', gt[ spras = 'R' ] color col_key , / 'LTEXT in 1st line', gt[ spras = 'E' ]-ltext color col_key , / 'ACTVT in 2st line', gt[ spras = 'R' ]-actvt color col_key. gt[ 1 ]-ltext = 'HO-HO-HORSE'. gt[ 2 ]-actvt = '++'. skip. write: / 'Whole 1st line ', gt[ spras = 'E' ] color col_key , / 'Whole 2nd line ', gt[ spras = 'R' ] color col_key , / 'LTEXT in 1st line', gt[ spras = 'E' ]-ltext color col_key , / 'ACTVT in 2st line', gt[ spras = 'R' ]-actvt color col_key.
Лист. 18. Ключевые выражения. (Так называемые «ключевые» — если первую строку заменить второй)
А почему же «так называемые» ключевые выражения? Да потому, что они работают, даже по тем полям, которые не вошли в ключ. Чтобы это проверить, закомментируйте первую строку и уберите комментаторную звездочку из второй строки.
На самом деле «ключевость» используется, если поля в выражении в квадратных скобках являются полями первичного ключа, взятыми слева направо подряд без пропусков. Можно использовать ключевые выражения для полей дополнительного ключа (secondary key), но в таком случае нужно явно указать, каким из дополнительных ключей следует пользоваться. Имя дополнительного ключа указывается после ключевого слова KEY, а после ключевого слова COMPONENTS указываются имена полей ключа и через знак равенства их значения.
Как это работает можно увидеть на пример нижеследующего отчетика (Лист. 19).
: data : gt type standard table of tactt with key spras actvt
with non-unique sorted KEY text COMPONENTS ltext
, gs like line of gt
, gx type ref to cx_sy_itab_line_not_found.
parameters: p_spras type tactt-spras default 'E'
, p_actvt type tactt-actvt default '01'
, p_ltext type tactt-ltext default 'Delete'.
start-of-selection.
select * from tactt into table gt where spras in ('D','E','R').
try.
gs = gt[ KEY text COMPONENTS ltext = p_ltext ].
write: / gs color col_key.
catch cx_sy_itab_line_not_found into gx.
write: / gx->get_text( ) color col_negative.
endtry.
try.
gt[ key text components ltext = p_ltext ] = '#++ <<Renewed>>'.
write: / gt[ KEY text COMPONENTS ltext = ' <<Renewed>>' ]
color col_key.
catch cx_sy_itab_line_not_found into gx.
write: / gx->get_text( ) color col_negative.
endtry.
Лист. 19. Ключевые выражения. (На этот раз действительно «ключевые»)
Согласившись с умалчиваемыми значениями, получим результат (см. Рис. 16).

Рис. 16. Результат Лист. 19
4.2. MOVE-CORRESPONDING для внутренних таблиц
Команда (стэйтмент) MOVE-CORRESPONDING теперь может работать не только с компонентами структур, но и с внутренними таблицами. По этой команде из источника в приемник переносятся одноименные столбцы. По умолчанию содержимое приемника удаляется. Если желательно добавить новое содержимое из источника, сохраняя при этом прежнее содержимое приемника, то используется фраза KEEPING TARGET LINES (Лист. 20, результат — Рис. 17).
data: gt1 type standard table of tactt , gt2 type standard table of tact. start-of-selection. select * from tactt into table gt1 where spras = 'E' and actvt < '04'. write: / 'Source'. loop at gt1 into data(gz). write: / gz. endloop. skip. write: / 'Target losing its initial line(s)'. append 'QK' to gt2. MOVE-CORRESPONDING gt1 to gt2. loop at gt2 into data(gs). write: / gs. endloop. skip. write: / 'Target keeping its initial line(s)'. refresh gt2. append 'QK' to gt2. MOVE-CORRESPONDING gt1 to gt2 KEEPING TARGET LINES. loop at gt2 into gs. write: / gs. endloop.
Лист. 20. MOVE-CORRESPONDING для внутренних таблиц

Рис. 17. Результат Лист. 20
4.3. Фраза BASE в операторах VALUE и NEW для внутренних таблиц
Выше в разделе (в разделе 3.1) мы уже упомянули, что оператор-конструктор VALUE позволяет заполнить внутреннюю таблицу. Кроме того, он позволяет добавить в стандартную или вставить в сортированную или хешированную внутреннюю таблицу новые данные, сохраняя старое содержимое. Для этого используется ключевое слово BASE, после которого указывается им той же внутренней таблицы (Лист. 21, результат Рис. 18).
types : begin of ts_pet , species type char10 , name type char20 , end of ts_pet , tt type standard table of ts_pet with default key. data gt type standard table of ts_pet. start-of-selection. gt = VALUE #( ( species = 'Cat' name = 'Tiglatpalassar' ) ). loop at gt into data(pet). write / pet. endloop. skip. write: / 'Replacing with VALUE(...)'. gt = VALUE #( ( species = 'Dog' name = 'Fido' ) ). loop at gt into pet. write / pet. endloop. skip. write: / 'Adding with VALUE( BASE ... )'. gt = VALUE #( BASE gt ( species = 'Cat' name = 'Tom' ) ). loop at gt into pet. write / pet. endloop.
Лист. 21. Фраза BASE в операторе VALUE для внутренних таблиц

Рис. 18. Результат Лист. 21
Если же после ключевого слова BASE указать имя другой внутренней таблицы совместимой структуры, то тогда в приемник будет сначала помещено ее содержимое, а потом то, что указано в следующей далее части выражение конструктора.
4.4. Селекция записей итератором FOR…WHERE в операторах VALUE и NEW для внутренних таблиц
Операторы конструкторы VALUE и NEW позволяют переносить в приемник не все записи из источника, а по указанному условию. Для этого в выражении конструктора используется итератор FOR…WHERE. Для примера, создадим внутреннюю таблицу из собак и кошек, а потом развалим ее на две: кошки отдельно, собаки отдельно (Лист. 22, результат — Рис. 19).
types: begin of ts_pet , species type char10 , name type char20 , end of ts_pet . types tt type standard table of ts_pet with empty key. start-of-selection. data(gt) = value tt( ( species = 'Cat' name = 'Tiglatpalassar' ) ( species = 'Dog' name = 'Fido' ) ( species = 'Dog' name = 'Zlobermann' ) ( species = 'Cat' name = 'Basil' ) ( species = 'Dog' name = 'Daisy' ) ). data(gt_cats) = value tt( FOR cat IN gt WHERE ( species = 'Cat' ) ( cat ) ). data(gt_dogs) = value tt( FOR dog IN gt WHERE ( species = 'Dog' ) ( dog ) ). break-point.
Лист. 22. Фраза FOR…WHERE — селектор в операторе VALUE для внутренних таблиц

Рис. 19. Результат Лист. 22
4.5. Порождение записей итератором FOR…THEN в операторах VALUE и NEW для внутренних таблиц
Применение итератора FOR…THEN позволяет заполнять внутренние таблицы данными, вычисленными этим итератором. Не то, чтобы это было очень часто нужно, но возможность красивая. Для примера затабулирую функцию гиперболического косинуса в интервале от -10 до 10 с шагом 1 и построю ее график одним (правда довольно длинным) оператором-конструктором VALUE (Лист. 23, результат — Рис. 20).
types : begin of ts , xx type p decimals 3 , ff type p decimals 3 , gg type string , end of ts , tt type standard table of ts with empty key. start-of-selection. data(gt) = value tt( FOR i = -10 THEN i + 1 WHILE i <= 10 ( xx = i ff = ( exp( i / 10 ) + exp( - i / 10 ) ) / 2 gg = `|` && repeat( val = ` ` occ = ( exp( i / 10 ) + exp( - i / 10 ) ) * 20 - 1 ) && `*` ) ). write: / 'Hyperbolic cosine: y = ch( x )'. loop at gt into data(gs). write: / gs-xx, gs-ff, gs-gg color col_key. endloop.
Лист. 23. Фраза FOR…THEN — итератор в операторе VALUE для внутренних таблиц

Рис. 20. Результат Лист. 23
4.6. Мэши
Довольно часто бывает нужно иметь дело с данными разного уровня. Например, с заголовком документа и его позициями. И тогда обычно приходится делать одно из двух. Либо все держать в одной внутренней таблице. Но тогда она оказывается избыточной, поля, относящиеся к заголовку, будут дублироваться для каждой рядовой записи. Либо создавать несколько внутренних таблиц и делать по ним вложенные циклы (или внутри цикла по одной таблице читать одиночные данные другой таблицы). Можно, конечно, делать внутренние таблицы, в клетках записей которых содержатся другие внутренние таблицы. Но это вариант второго подхода. Кстати, сам SAP такое использует, например, в управлении свойствами (цветами, стилями) отдельных клеток грида класса CL_GUI_ALV_GRID. Способы довольно громоздкие. И вообще вложенные циклы часто вызывают недовольство программистов и специалистов контроля качества программ.
О счастье! Теперь появился новый тип данных — MESH, который позволяет во многих случаях убрать вложенные циклы из обхода внутренних таблиц. Мэш — это составной тип данных, компонентами которого являются внутренние таблицы, между которыми установлены ассоциации. По ассоциациям можно накладывать условия. Это похоже на джойн. Только в нем участвуют не таблицы базы данных, а внутренние таблицы.
Чтобы показать, как это делается, сделаем мэш из двух таблиц. В одной будут собаки и кошки с длинами их хвостов, а в другой продукты обихода для их жизни дома. Устроим мэш с ассоциацией по имени животного (Лист. 24, состав мэша и его конмпонентов — Рис. 21, результат — Рис. 22).
types: begin of ts_pet , species type char10 , name type char20 , tail type p length 4 decimals 2 , end of ts_pet , begin of ts_prod , name type char20 , product type char20 , count type i , end of ts_prod , tt_pet type sorted table of ts_pet with unique key name , tt_prod type standard table of ts_prod with empty key . types: begin of MESH tm_mesh , prod type tt_prod ASSOCIATION prod_to_pet to pet on name = name , pet type tt_pet , end of MESH tm_mesh. data gm_mesh type tm_mesh. start-of-selection. " Tables filling data(gt_pet) = value tt_pet( ( species = 'Dog' tail = '27.5' name = 'Fido' ) ( species = 'Dog' tail = '42.5' name = 'Zlobermann' ) ( species = 'Cat' tail = 21 name = 'Basil' ) ). data(gt_prod) = value tt_prod( ( name = 'Fido' product = 'Paw plunger' count = 1 ) ( name = 'Basil' product = 'Cat haus' count = 1 ) ( name = 'Basil' product = 'Toy mouse' count = 3 ) ( name = 'Basil' product = 'Tail cleaner' count = 1 ) ). " Mesh components filing gm_mesh-pet = gt_pet. gm_mesh-prod = gt_prod. break-point. "Examine GM_MESH " Mesh components display loop at gm_mesh-prod into data(gs_prod). write: / gs_prod-name, gs_prod-product, gs_prod-count. endloop. skip. loop at gm_mesh-pet into data(gs_pet). write: / gs_pet-name, gs_pet-species, gs_pet-tail. endloop. skip. " Loop at Mesh loop at gm_mesh-prod into gs_prod. data(pet_detl) * = gm_mesh-prod\prod_to_pet[ gm_mesh-prod[ name = gs_prod-name ] ]. = gm_mesh-prod\prod_to_pet[ gs_prod ]. write: / gs_prod-name, gs_prod-product, gs_prod-count , pet_detl-species, pet_detl-tail. endloop.
Лист. 24. Простой пример мэша

Рис. 21. Состав мэша Лист. 23 и его компонентов

Рис. 22. Результат Лист. 24. Последняя тетрада — результат цикла по мэшу
4.7. Оператор конструктор REDUCE
Специально для внутренних таблиц появился оператор конструктор REDUCE. Он позволяет подводить итоги по внутренним таблицам и группировкам внутри внутренних таблиц. Но его использование заслуживает отдельного не просто пристального, а придирчивого рассмотрения в сравнении с AT-группами в циклах по внутренним таблицам.
5. Перечисления (ENUM)
Перечислимые типы позволяют задавать значения в виде имени, соответствующие значения похожи на константы. Ну, например для указания дней недели мы в речи используем названия дней. Редко кому приходит в голову использовать номера дней недели в разговорной речи. А в программировании — дни недели зачастую обозначаются целым числом. При этом существует вероятность ввести неподходящее значение, например 8. Ошибочность ввода выяснится лишь во время исполнения программы. Перечислимые типы соотносят со значениями имена, в этом они подобны константам. Переменные перечислимых типов синтаксически не могут иметь значений, отличающихся от заданных в определении типа. Это проверяется еще при синтаксическом контроле и до исполнения программы и передача недопустимых значений становится синтаксически невозможной. Что, конечно же, уменьшает количество ошибок программирования и пользовательского ввода.
С версии 7.5 перечислимые типы появились в ABAP (Лист. 25).
TYPES : BEGIN OF ENUM te_weekday , monday , tuesday , wednesday , thursday , friday , saturday , sunday , END OF ENUM te_weekday . DATA : gv_day0 TYPE te_weekday , gv_day1 TYPE te_weekday VALUE monday , gv_day2 TYPE te_weekday VALUE friday , gv_week TYPE string . * parameters p_day type te_weekday. " Impossible !!! * parameters p_day like gv_day0. " Impossible !!! . START-OF-SELECTION. WRITE: / ' 123456789+123456789+123456789+' , / 'gv_day0', gv_day0 COLOR COL_KEY , / 'gv_day1', gv_day1 COLOR COL_KEY , / 'gv_day2', gv_day2 COLOR COL_KEY . SKIP. WRITE: / 'gv_day0 = sunday.'. gv_day0 = sunday. WRITE: / 'gv_day0', gv_day0 COLOR COL_KEY. SKIP. WRITE: / 'gv_day0 = gv_day2.'. gv_day0 = gv_day2. WRITE: / 'gv_day0', gv_day0 COLOR COL_KEY. SKIP. gv_week = monday && ` ` && tuesday && ` ` && wednesday && ` ` && thursday && ` ` && friday && ` ` && saturday && ` ` && sunday. WRITE: / gv_week.
Лист. 25. Перечисление ENUM
6. Вместо заключения
Мы тут весьма вкратце рассмотрели некоторые новости языка ABAP, появившиеся с версий 7.4 и 7.5: инлайновые декларации, операторы-конструкторы, многие (но не все) новшества, связанные с внутренними таблицами, перечислимые типы.
Об этом я рассказываю на мастер-классе «Новый синтаксис ABAP» и показываю, как это делать.
Конечно же есть и другие приятные новости. За бортом остались:
- оператор-конструктор REDUCE, позволяющий значительно сократить текст вычисления итогов по внутренним таблицам и под-итогов по группировкам;
- новый синтаксис SELECT, более ясный, чем старый и переносящий часть обработки из ABAP-программы на уровень базы данных, что особенно приятно при работе с быстрыми базами данных SAP HANA и Oracle;
- глобальные временные таблицы, позволяющие перенести на уровень базы часть обработки;
- каналы сообщений, устанавливающие связь между программами
- классы AMDP, позволяющие писать хранимые процедуры и табличные функции на языке базы данных SQLScript SAP HANA.
Каждая из этих тем заслуживает пристального внимания, своих статей и мастер-классов, и даже отдельных книг учебных курсов. Если к этому будет интерес, сообщите.
Есть и множество мелких и тем не мнее приятных новшеств, заслуживающих по меньшей мере упоминания. Ну например, появление нового типа данных длинных восьмибайтовых целых чисел. Интересующихся отсылаю к самой главной транзакции ABAP-разработчика — ABAPDOCU (см. Рис. 23).

Рис. 23. Транзакция ABAPDOCU. Новости
Об авторе

Василий Ковальский — эксперт по обучению ABAP. Преподавал ABAP-курсы в московском подразделении SAP с 1998 года до закрытия московского подразделения SAP. В настоящее время сотрудничает с ExpertRP, разработал и преподает линейку авторских ABAP-курсов, проводит мастер-классы и вебинары по ABAP. Связаться с Василием можно по адресу.