ABAP в XXI веке (часть 2)
Это вторая часть публикации Карстена Больмана, посвященной функциональным возможностям ABAP в дополнениях версии 7.40 к программному языку ABAP. Здесь рассматриваются внутренние таблицы, получение доступа к таблицам и их создание, а также агрегация, группирование и трансформация. Целью большинства расширений является смягчение жесткой императивной парадигмы.
Ключевое понятие
ABAP — типичный язык процедур, который можно усовершенствовать, ориентировав на объект. Все чаще в современных языках учитывается тот факт, что многие задачи программирования можно рассматривать как исключительно вычисление значений, кратко и точно реализованных в виде выражений.
Если вас заинтересовала первая часть обзора функций языка ABAP 7.40, вам непременно стоит прочитать и вторую часть. Эта часть посвящена внутренним таблицам. Этот гибкий и мощный инструмент относится к самым основам ABAP, без внутренних таблиц язык стал бы бесполезным.
К сожалению, структуры ABAP для создания таблиц и обращения к ним до сих пор проходят эволюционный этап операторов «MOVE x TO y» и «COMPUTE X = y + 1», о которых шла речь в первой части: Они избыточны и неортогональны. Применяя их, мы думаем о микровоздействии на состояние, а не о наборах команд для создания значений на основе других значений, что было бы целесообразно для значительной доли задач программирования.
При работе с таблицами приходится сталкиваться с такими операторами:
READ TABLE tab WITH KEY c1 = ’a’ ASSIGNING <line>.
LOOP AT tab1 ASSIGNING <line> WHERE c1 = ’a’.
INSERT <line>-c2 INTO TABLE tab2.
ENDLOOP.
Вспомним, как задания (преобразование типа и создание структурированных значений) были вызволены из императивного мрака выражениями конструктора:
use_string( CONV string( char10 ) ).
use_struct( VALUE t_struc( c1 = ’a’ c2 = 10 ) ).
Такой вариант поддерживался концепциями линейного описания и вывода типа, направленными на избавление от избыточности. Табличные операторы уже сейчас пользуются преимуществами этих концепций:
READ TABLE tab INDEX 1 ASSIGNING FIELD-SYMBOL(<line>).
INSERT VALUE #( c1 = 'a' c2 = 10 ) INTO TABLE tab.
Мы уже знаем, как создавать таблицы с фиксированным числом строк в виде значений:
use_inttab( VALUE #( ( x - 1 ) ( x ) ( x + 1 ) ) ).
Нам необходимо рассмотреть два важных момента:
- Доступ к табличным строкам без операторов для манипуляций с состоянием, например, READ.
- Создание табличных значений с переменным количеством строк (например, на основе строк другой таблицы).
Далее мы перейдем к более сложным задачам: сокращение таблицы до нетабличного значения, обработка табличных строк в группах (вспомните GROUP BY для SQL) и выполнение с таблицами старой доброй операции MOVE-CORRESPONDING с пользовательским мэппингом.
Выбор табличных строк
Как известно, только один вариант синтаксиса для обращения к таблице не сбивает с толку 90% программистов: квадратные скобки. Для обращения к первой строке таблицы следует написать
tab[ 1 ]
Да, это новый синтаксис ABAP для «READ TABLE tab INDEX 1».
Этот новый вид выражений работает во всех позициях грамматики ABAP с применением выражений (которая сводится к операндам чтения неустаревших непериферийных операторов). Но выбранная строка таблицы также является l-значением, сопоставимым в этом отношении с выражениями NEW и CAST, описанными в первой части:
tab[ 1 ] = struc.
tab[ 2 ]-c1 = 'a'.
Первая строка является присвоением, которое изменяет выбранную строку таблицы в целом, а вторая строка показывает изменение только одного компонента строки путем сцепления выбранной по номеру строки таблицы с компонентом через символ дефиса «–» и последующим именем поля таблицы.
В «[…]» доступны два способа выбора: по индексу или по значению(ям) компонента. Для второго способа применяется следующий формат:
tab[ c1 = x c2 = f( y ) ]
Если критерию поиска соответствуют несколько строк, выбор строки определяется порядком вставки (аналогично для READ). Оба варианта могут относиться к явному ключу, определенному для типа таблицы:
tab[ KEY key1 INDEX 1 ]
tab[ KEY key1 c1 = x c2 = f( y ) ]
Первичный или вторичный ключ, используемый для доступа к индексу, не должен являться хеш-ключом. Компоненты явного ключа должны совпадать, например, как в операторе «READ TABLE … WITH TABLE KEY». В противном случае выражение получает семантику произвольного ключа «READ TABLE … WITH KEY» с имплицитным использованием первичного ключа по возможности. Компоненты, как и имя ключа, могут быть динамическими:
tab[ KEY (keyname) c1 = x (compname) = y ]
Таким образом, для большинства разновидностей оператора READ существуют функциональные эквиваленты. (Варианты «FROM workarea» и «BINARY SEARCH» не поддерживаются.)
У разработчика на ABAP возникает естественный вопрос: Это READ … INTO или READ … ASSIGNING (т.е. я получаю копию строки или указатель на нее)?” Ответ: Обычно это неважно, компилятор учитывает контекст и тип строки:
- Семантика указателя задается определенным контекстом (например, вызов метода, см. ниже).
- Кроме того, если строка относится к родовому типу, указатель становится единственной опцией.
- Если тип строки широкий или глубокий, компилятор выбирает семантику указателя, поскольку в данном случае это эффективнее. Если тип строки узкий и плоский, вы получаете семантику копии. Такая оптимизация применима в большинстве позиций значения r.
В вызове метода компилятор использует семантику указателя по умолчанию. (Семантика копии как неявная оптимизация может привести к непредвиденному поведению в результате побочных эффектов.) Однако если тип строки узкий и плоский, Extended Syntax Check (SLIN) выдает предупреждение с рекомендацией выбрать VALUE. Эта рекомендация подразумевает следующий вариант синтаксиса:
VALUE #( tab[ … ] )
Для выбора таблицы строка копируется из таблицы в отдельное значение. Как правило, этой рекомендации SLIN стоит последовать. Аналогичный вариант
REF #( tab[ … ] )
получает ссылку на нужную строку (например, если требуется параметр типа строки REF TO).
Это новый внутренний синтаксис для операторов VALUE и REF из первой части публикации. Дополнительного оператора для ASSIGNING не предусмотрено; это значение по умолчанию. Но как явно присвоить строку таблицы символу поля, возможно, совмещенному с линейный описанием? Например:
ASSIGN tab[ i ] TO FIELD-SYMBOL(<line>).
Выше я уже приводил пример сцепки компонентов. Выполняется обобщение по структурам данных с таблицами, вложенными на произвольную глубину. Для типов «таблица таблиц» цепочка выглядит как обращение к многомерному массиву:
ASSIGN matrix[ x ][ y ] TO <point>.
Либо цепочка включает в себя тире и имя компонента («-tab1», «-c3»):
tab0[ x ]-tab1[ c1 = y c2 = z ]-c3
Обратите внимание на сходство синтаксиса с цепочками для вызова метода:
meth0( x )->meth1( p1 = y p2 = z )->a
Вызов метода может находиться после, но не перед выбором таблицы (это приводит к появлению неэффективных структур, которые с помощью вызова метода извлекают всю таблицу вместо извлечения одной строки):
t1[ x ]-reftab[ y ]->meth( )
t2[ x ][ y ]-ref->meth( )->a
«meth( )[ x ] « не разрешено, это синтаксическая ошибка
Примеры цепочек наглядно показывают, как можно сократить громоздкий императивный код, наполненный вспомогательными переменными:
READ TABLE t1 INDEX x ASSIGNING FIELD-SYMBOL(<x>). « *yawn*
READ TABLE <x>-reftab INDEX y INTO DATA(ref). « zzzz…
ref->meth( ).
А что будет в случае сбоя при выборе? Очевидно, что для такой ситуации не предусмотрен код ошибки в SY-SUBRC. Побочные эффекты выражений – вещь крайне неприятная. Концепция выражения никогда не изменяет поля SY. Вместо этого проблемный выбор таблицы генерирует особую ситуацию класса CX_SY_ITAB_LINE_NOT_FOUND. Объект особой ситуации содержит информацию о предмете сбоя (например, какой из индексов). В ряде случаев, но не всегда, это позволяет выявить проблемный выбор в цепочке. Если требуется усилить контроль, разделите цепочку. Например:
ASSIGN t2[ x ] TO FIELD-SYMBOL(<x>).
CHECK sy-subrc = 0.
ASSIGN <x>[ y ] TO FIELD-SYMBOL(<y>).
В отличие от выбора на уровне выражений оператор ASSIGN, по собственной традиции, устанавливает для SY-SUBRC значение 0 (успех) или 4 (сбой), на которое можно реагировать в потоке управления.
Если вы подготовились к тому, что искомой строки не существует, и определили для такого случая значение по умолчанию, на помощь снова приходит оператор VALUE:
VALUE #( tab[ x ][ y ] DEFAULT deflt )
В случае сбоя любого выбора в цепочке выдается значение deflt. Для получения начального значения в данном случае пишите так:
VALUE #( tab[ x ][ y ] OPTIONAL )
Для тестирования наличия строки используется встроенная предикативная функция:
CHECK line_exists( tab[ c1 = x ] ).
Если требуется индекс найденной строки:
DATA(i) = line_index( tab[ c1 = x ] ).
Если строка не найдена, возвращается значение 0. Обе функции используются для выбора в цепочке. В этом случае результат относится к окончательному выбору в цепочке. Ни одна из них не генерирует особые ситуации.
Примечание. Удобство выражений вводит в заблуждение. Когда запись операций не требует серьезных усилий, могут появиться следующие конструкции:
tab1[ idx1 ]-a = tab2[ idx2 ]-x.
tab1[ idx1 ]-b = tab2[ idx2 ]-y.
tab1[ idx1 ]-c = tab2[ idx2 ]-z.
К сожалению, компилятор ABAP по-прежнему не силен в оптимизации. Например, он не выполняет обычное исключение подвыражений. Таким образом, вам придется самостоятельно факторизировать промежуточные результаты, либо пострадает время выполнения. В этом примере шесть вариантов выбора таблиц могут быть сокращены до необходимых двух следующим образом:
ASSIGN tab1[ idx1 ] TO FIELD-SYMBOL(<r1>).
ASSIGN tab2[ idx2 ] TO FIELD-SYMBOL(<r2>).
<r1>-a = <r2>-x. <r1>-b = <r2>-y. <r1>-c = <r2>-z.
Генератор таблиц
Мы рассмотрели обращение к табличным строкам. Следующей задачей является создание таблиц в виде значений без обращения к варианту «много мелкий манипуляций с состоянием»:
LOOP AT tab ASSIGNING FIELD-SYMBOL(<x>) WHERE c0 = ’X’.
INSERT VALUE #( d1 = <x>-c1 d2 = <x>-c2 )
INTO TABLE tab1.
ENDLOOP.
Необходимая концепция давно известна и формально уходит корнями в нотацию математических множеств:
{ f (x) | x Î S, P(x) }
Вычисляется ли набор значений f (x) функцией результата f для всех элементов x из исходного набора S, для которых верен предикат P(x). В случае применения к контейнеру списка (распространено во многих функциональных языках, например, в Haskell) это называется генерацией списка. В ABAP роль контейнера играет внутренняя таблица, поэтому название было изменено соответственно. Синтаксически получаемое выражение f (x) перемещается в конец. Таким образом, генерация таблиц становится плавной генерализацией создания таблиц с помощью оператора VALUE (Рис. 1).
- Условие FOR вводит LOOP на уровне выражения. Оно привязывает локальный символ поля (с именем «<…>») для семантики LOOP ASSIGNING или локальную переменную для семантики LOOP INTO. Как было сказано в первой части, локальную переменную можно повторно использовать в других выражениях, но не уровне оператора. Следует четко разграничивать локальные и нелокальные переменные.
- Выражение после IN указывает исходную таблицу, строки которой, в свою очередь, привязаны к символу FOR.
- Дополнительно проверенные стоки можно сократить до подмножества:
- Условие FROM /TO указывает диапазон индексов и исходной таблице.
- Условие WHERE фильтрует строки по логическому выражению. (Круглые скобки вокруг логического выражения обязательны, в противном случае возможны проблемы с синтаксическим анализом.)
- Перед этими условиями может стоять условие USING KEY (не показано) для указания ключа таблицы для этих ограничений.
- Условие LET для привязки локальных переменных уже знакомо нам по первой части статьи. В генераторе таблиц оно, как правило, используется для предотвращения множественных вычислений значения из текущей строки (для ссылки используется символ FOR). На Рис. 1 обратите внимание на привязку выбора таблицы (l-значение!) к локальному символу поля (не переменной). Это позволяет избежать копирования семантики для табличного поиска.
- Наконец, далее следует одна или несколько спецификаций строк, как в статическом случае, но с использованием символа FOR.
Оформите подписку sappro и получите полный доступ к материалам SAPPRO
Оформить подпискуУ вас уже есть подписка?
Войти