Modernes ABAP

Konstruktor-Ausdrücke

(C) Brandeis Consulting.

Einleitung

Die Konstruktor-Ausdrücke sind eine sehr nützliche und mächtige Erweiterung der ABAP Sprache. Sie beginnen immer mit einem Konstruktor-Operator.

Gemeinsamkeiten der Konstruktor-Ausdrücke

  • Alle Konstruktor-Ausdrücke liefern einen Wert zurück.
  • Die Syntax ist immer
    <Konstruktor-Operator> <Datentyp>( ... )

Einfaches Beispiel mit dem VALUE Operator

TYPES: BEGIN OF ts_data,
          a     TYPE i,
          b(10) TYPE c,
        END OF ts_data.
TYPES tt_data TYPE STANDARD TABLE OF ts_data WITH DEFAULT KEY.
DATA  lt_data TYPE tt_data.

lt_data = VALUE tt_data( ( a = 1  b = 'ABC'  )
                         ( a = 13 b = 'Hallo') ).

(C) Brandeis Consulting.

Liste der Konstruktor-Operatoren

  • VALUE und NEW erzeugen neue Datenobjekte, wobei letzterer eine Referez darauf liefert.
  • COND und SWITCH sind Fallunterscheidungen. Sie liefern abhängig von logsichen Bedingungen unterschiedliche Werte.
  • CORRESPONDING erzeugt ein neues Datenobjekt auf Basis eines bestehenden Datenobjektes
  • REF erzeugt eine Referenz wie GET REFERENCE OF
  • EXACT - Führt Berechnungen nur dann durch, wenn sie ohne Rundungsfehler sind. Ansonsten wird eine Ausnahme erzeugt
  • REDUCE erzeugt aus internen Tabellen einen Wert
  • CONV und CAST konvertieren Datentypen
  • FILTER erzeugt eine neue interne Tabelle durch Filterung auf eine bestehende interne Tabelle
(C) Brandeis Consulting.

Der Datentyp von Konstruktor-Ausdrücken

Expliziter Datentyp

Es kann ein expliziter Datentyp angegeben werden, beispielsweise ein

  • Datenelement
  • Tabellentyp
  • Strukturtyp
  • Klassenname

Datentyp Ableitung mit dem #

Wenn statt einem konkreten Datentyp das Hash-Zeichen # angegeben wird, versucht das System den Datentypen aus dem Kontext zu ermitteln. z.B.

  • Bei einer Zuweisung aus dem Datentypen der lhs erwartet wird
  • Bei einem Methodenparameter der entsprechende Typ

Im Prinzip das Gegenstück zu den Inline-Deklarationen mit DATA(...), wo der Datentyp sich auch aus dem Kontext ergibt.

Beispiel für EXACT - Klasse ZBC_OTHER_EXPRESSIONS

    TRY.
        DATA(result) = EXACT int4( 10 / input ).
        out->write( |The result is exact { result }| ).

      CATCH cx_sy_conversion_rounding  INTO DATA(lx_conv).
        out->write( |The result was rounded to { lx_conv->value }| ).

    ENDTRY.
(C) Brandeis Consulting.

VALUE Operator - Übersicht

Ohne Argumente

Ohne weitere Argumente erzeugt der VALUE Operator ein initiales Datenobjekt für alle Datentypen.

Strukturen

VALUE structype( feld1 = wert1 feld2 = wert2 ... )

Tabellen

VALUE tabletype( ( feld1 = wert1 feld2 = wert2... ) " Zeile 1 
                 ( ... )                            " Zeile 2
                 ...                              )
(C) Brandeis Consulting.

VALUE Operator mit konstanten Daten

Der wichtigste Operator, weil er das Konstruieren von internen Tabellen mit festem Inhalt erlaubt ohne 100x APPEND.

    DATA lr_data TYPE RANGE OF dats.

    lr_data = VALUE #( ( low = sy-datum sign = 'I' option = 'GE' )
                       ( low = '99991231' sign = 'I' option = 'LE' ) ).

    SELECT *
      FROM zbc_tasks
      WHERE due_date IN @lr_data
      INTO TABLE @DATA(result).

Insbesondere bei Unit-Tests ist das extrem wichtig, da wir somit ganz einfach Testdaten und Vergleichsdaten erzeugen können. Statt der Literale können wir uns natürlich auch auf Variablen beziehen:

(C) Brandeis Consulting.

Interne Tabelle mit BASE in VALUE übernehmen

Falls in die neue Tabelle Zeilen einer bestehenden Tabelle hinzugefügt werden sollen, so kann man das mit dem Schlüsselwort BASE erreichen:

data(first_table) = value tt_demo( curr = 'EUR' account = '12322' ( date = sy-datum     amount = 123 )
                                                                  ( date = sy-datum + 1 amount = 130 ) ).
out->write( name = |\nFirst Table| data = first_table ).

data(second_table) = value tt_demo( base first_table
                                    curr = 'EUR' account = '77777' ( date = sy-datum     amount = 12 )
                                                                   ( date = sy-datum + 1 amount = 11 ) ).
out->write( name = |\nSecond Table| data = second_table ).
First Table  
DATE        AMOUNT  ACCOUNT  CURR  
2022-09-29  123.0   12322    EUR   
2022-09-30  130.0   12322    EUR   

Second Table  
DATE        AMOUNT  ACCOUNT  CURR  
2022-09-29  123.0   12322    EUR   
2022-09-30  130.0   12322    EUR   
2022-09-29  12.0    77777    EUR   
2022-09-30  11.0    77777    EUR 
(C) Brandeis Consulting.

Beispiel VALUE- Operator in Bex Variablen-Exits

Klassisch

DATA: loc_var_range type RRRANGEEXIT,
      l_s_range type rrrangesid,
      en_year(4) type c,
      f_day_year(8) type c,
      en_mmdd(4) type c,
      range_low(8) type c.  
IF I_STEP = 2. 
LOOP AT i_t_var_range INTO loc_var_range 
                      WHERE VNAM = 'ZENTRYDATE'.
clear l_s_range. 
range_low = loc_var_range-low.
en_year = range_low+0(4).
en_year = en_year - 1. 
en_mmdd = range_low+4(4). 
clear range_low.
CONCATENATE en_year '0101' INTO f_day_year.
CONCATENATE en_year en_mmdd INTO range_low.
l_s_range-low = f_day_year.
l_s_range-high = range_low.
l_s_range-sign = 'I'.
l_s_range-opt = 'BT'. 
APPEND l_s_range TO e_t_range.
ENDLOOP.
ENDIF.

Mit VALUE Operator

if i_step = 2. 
 try. 
  data(entrydate) = i_t_var_range[ vnam = 'ZENTRYDATE' ]-low. 

  data(lastyear) = |{ entrydate(4) - 1 }|. 
  data(monthday) = entrydate+4(4).

  et_range = value #( base et_range 
                    ( low  = lastyear && '0101' 
                      high = lastyear && monthday 
                      sign = 'I'
                      opt  = 'EQ' ) )
  catch cx..
    "Variable not found. 
  endtry. 
endif. 
(C) Brandeis Consulting.

VALUE bei Tabellen: Gemeinsame Komponenten

Werte die in mehreren Zeilen identisch sind, können vor der ersten Zeile mit diesem Wert definiert werden:

    lt_range = value #( sign = 'I' option = 'EQ' ( low = '20221031'  )
                                                 ( low = '20230406'  )
                                   option = 'BT' ( low = '20221102'  high = '20221104' )
                                                 ( low = '20221221'  high = '20230107' )
                                                 ( low = '20230530'  high = '20230609' )
                                                 ( low = '20230727'  high = '20230909' ) ).

Felder die ausserhalb gefüllt wurden, dürfen in den folgenden Zeilen nicht mehr innerhalb der Klammer gefüllt werden.

(C) Brandeis Consulting.

Zählerschleife mit FOR in Ausdrücken

    DATA lt_int TYPE TABLE OF i.

    lt_int = VALUE #( FOR i = 1 
                      THEN i + 1
                      UNTIL i >= 10 
                      ( i )  ).
    
    out->WRITE( lt_int ).

.

  • Nach FOR wird eine Variable initialisiert
  • Hinter THEN kann die Variable verändert werden
  • Mit UNTIL oder WHILE wird eine Abbruchbedingung formuliert
  • Nach der Abbruchbedingung erfolgt ein passender Ausdruck. Bei VALUE wird hier meist eine Zeile erzeugt
(C) Brandeis Consulting.

Tabellenschleifen mit FOR in Ausdrücken

Mit FOR <Zeile> IN <Tabelle> kann man eine Schleife über die Zeilen einer internen Tabelle erzeugen:

SELECT summary,
        task_id
  FROM zbc_tasks
  INTO TABLE @DATA(lt_data)
  UP TO 10 ROWS.

TYPES tt_tmp LIKE lt_data.

DATA(lt_tmp) = VALUE tt_tmp( FOR ls_data IN lt_data WHERE ( task_id < 4  )
                              ( summary = to_upper( ls_data-summary ) task_id = 42 )  ).

out->write( lt_tmp ).

  • Die WHERE Bedingung ist optional. Sie muss in Klammern gesetzt werden.
  • Gruppenstufenverarbeitung ist mit FOR GROUPS möglich
  • Optional kann der aktuelle Index der Schleife in eine Variable mit INDEX INTO <Indexvariable> übergeben werden
(C) Brandeis Consulting.

NEW

Der NEW Konstruktor-Operator lässt sich auf zweierlei Arten nutzen:

  1. Zur Instanzerzeugung von Objekten mit
    NEW <Klassenname>( <Konstruktorparameter> )
  2. Zur Erzeugung von Datenobjekten mit Referenz
    data(lr_tmp) = new tt_demo( ( account = '123' amount = '12.34' curr = 'EUR' )
                                ( account = '125' amount = '12.34' curr = 'EUR' ) ).
    assign lr_tmp->* to field-symbol(<fs>).
    out->write( <fs> ).

Der NEW Konstruktor-Operator ist ansonsten ähnlich dem VALUE Operator:

  • Mit BASE können Werte aus einer Tabelle übernommen werden
  • Mit FOR können Schleifen erzeugt werden
(C) Brandeis Consulting.

Der CONV Konstruktor-Operator

Mit dem CONV Operator kann der Datentyp eines Datenobjektes konvertiert werden.
CONV <Zieldatentyp>( <Datenobjekt> ).

Das ist insbesondere dann praktisch, wenn man ansonsten eine neue Hilfsvariable bräuchte, wie in diesem Beispiel:

Mit Hilfsvariable

  METHOD demo_conv.
    data lv_text type char8. 
    lv_text = sy-datum. 
    my_method( text =  lv_text ).
  ENDMETHOD.

Mit CONV Operator

  METHOD demo_conv.
    my_method( text = CONV #( sy-datum ) ).
  ENDMETHOD.

Gerade bei Methodenaufrufen kann der Ziel-Datentyp selbständig abgeleitet werden. Das ist extrem praktisch, aber auch gefährlich. Siehe Clean Code Hinweise.

(C) Brandeis Consulting.

Konvertierung von Objektreferenzen mit dem CAST Operator

Mit CAST können wir einen Up-Cast oder Down-Cast entlang der Vererbungshierarchie von ABAP Klassen durchführen.

  METHOD demo_cast.
    data ls_task type zbc_tasks. 
    
    data(lo_structdescr) = cast cl_abap_structdescr( cl_abap_structdescr=>describe_by_data( ls_task ) ). 
    
    data(lt_components) = lo_structdescr->get_components(  ).
    
  ENDMETHOD.

Falls die Konvertierung nicht möglich ist, wird eine Ausnahme vom Typ CX_SY_MOVE_CAST_ERROR erzeugt.

(C) Brandeis Consulting.

EXACT Ausdrücke

EXACT führt Zuweisungen nur dann durch, wenn sie ohne Verlust, d.h. Abschneiden von Zeichenketten oder Rundungsfehler sind. Ansonsten wird die Ausnahmeklasse CX_SY_CONVERSION_ERROR erzeugt.

EXACT führt Berechnungen nur dann durch, wenn sie ohne Rundungsfehler ist. Ansonsten wird die Ausnahmeklasse CX_SY_CONVERSION_ROUNDING erzeugt. Das gerundete Ergebnis kann man sich im CATCH Block ausgeben lassen.

(C) Brandeis Consulting.

Referenzen auf Daten mit REF erzeugen

Der Konstruktor-Operator REF erzeugt Datenreferenzen auf bestehende Objekte bzw. Tabellenausdrücke.
REF <Datentyp>|#( <Datenobjekt> )
Er entspricht somit weitgehend der bekannten Anweisung

GET REFERENCE OF <Datenobjekt> INTO <Referenzvariable>.

Zusammen mit Inline-Declarations kann man so eleganter an eine Referenzvariable kommen, insbesondere muss man den Typ häufig nicht kennen.

DATA(lr_components) = REF #( lt_components  ). 
(C) Brandeis Consulting.

Der CORRESPONDING Konstruktor-Operator

Mit CORRESPONDING kann man aus einem strukturierten Datenobjekt, also Tabelle oder Struktur, ein anderes Datenobjekt erzeugen und dabei die Werte gleichlautender Felder übernehmen. Das erinnert an MOVE-CORRESPONDING.

    DATA: BEGIN OF ls_task_small,
            task_id   TYPE zbc_tasks-task_id,
            summary   TYPE zbc_tasks-summary,
            new_field TYPE char10,
          END OF ls_task_small.

    SELECT SINGLE *
      FROM zbc_tasks
      INTO @DATA(ls_task_original) .

    "MOVE-CORRESPONDING ls_task_original TO ls_task_small.
    ls_task_small = corresponding #( ls_task_original ).
Alle Beispiel sind mit Strukturen. Sie gelten aber 1:1 auch für interne Tabellen.

SAP Dokumentation CORRESPONDING

(C) Brandeis Consulting.

CORRESPONDING mit BASE

Ein wichtiger Unterschied zu MOVE-CORRESPONDING ist, dass mit CORRESPONDING ein neues Datenobjekt erzeugt wird. Dort sind entsprechend alle Felder initial, die kein namensgleiches Feld in der Quelle haben.

Mit BASE kann eine Struktur benannt werden, die vorab in das Ergebnis des Konstruktor-Operators kopiert wird. Damit werden ggf. initiale Werte vorgefüllt.

Falls die Zielvariable angegeben wird, verhält es sich exakt wie bei MOVE-CORRESPONDING.

    ls_task_small = CORRESPONDING #( BASE ( ls_task_small )
                                     ls_task_original ).
(C) Brandeis Consulting.

CORRESPONDING mit MAPPING

Falls die Zielstruktur etwas anders lautende Felder hat, so können diese mit MAPPING explizit zugewiesen werden.

    DATA: BEGIN OF ls_task_small,
            id    TYPE zbc_tasks-task_id,
            title TYPE zbc_tasks-summary,
          END OF ls_task_small.

    SELECT SINGLE *
      FROM zbc_tasks
      INTO @DATA(ls_task_original) .
      
   ls_task_small = corresponding #( ls_task_original
                                    MAPPING id    = task_id
                                            title = summary ).
(C) Brandeis Consulting.

CORRESPONDING mit EXCEPT

Falls nicht alle gleichlautenden Felder in die Zielstruktur übernommen werden sollen, so können diese mit EXCEPT explizit ausgeschlossen werden.

    DATA: BEGIN OF ls_task_small,
            id    TYPE zbc_tasks-task_id,
            title TYPE zbc_tasks-summary,
            assignee type zbc_tasks-assignee,
          END OF ls_task_small.

    SELECT SINGLE *
      FROM zbc_tasks
      INTO @DATA(ls_task_original) .
      
   ls_task_small = corresponding #( ls_task_original
                                    MAPPING id    = task_id
                                            title = summary
                                    EXCEPT assignee ).
(C) Brandeis Consulting.

CORRESPONDING mit Lookup

Eine Spezialform des CORRESPONDING Operators ist ein Lookup auf eine andere interne Tabelle. Dieser entspricht weitgehend eines LEFT OUTER (Equi-) JOIN mit einer Kardinalität :1 in SQL. Das bedeutet im Einzelnen:

  • Wenn ein Datensatz in der Lookup-Tabelle gefunden wird, werden die Daten aus diesem nach den bekannten Regeln des CORRESPONDING Operators übernommen.
  • Wenn kein Datensatz gefunden wird, dann bleibt die Zeile der Ausgangstabelle unverändert.
  • Die JOIN-Bedingungen vergleichen immer mit =, zwischen den Bedingungen steht ein AND.
  • Die Zielfelder für den Lookup müssen schon in der Originaltabelle vorhanden sein.
    DATA lt_lookup TYPE HASHED TABLE OF I_CountryText WITH UNIQUE KEY country.
    DATA(lt_orig) = VALUE tt_demo( ( country = 'DE' )
                                   ( country = 'US' ) ).
    SELECT * FROM i_countrytext WHERE language = 'D' INTO TABLE @lt_lookup.

    DATA(lt_new) = CORRESPONDING tt_demo( lt_orig FROM lt_lookup
                                                  USING country = country
                                                  MAPPING country_text = CountryName  ).
    out->write( lt_new ).
(C) Brandeis Consulting.

Fallunterscheidungen mit dem SWITCH Operator

Der SWITCH Operator entspricht einem einfachen CASE Ausdruck in SQL. Welcher Wert erzeugt wird, hängt von Vergleichen eines festen Vergleichswerts mit konstanten Werten ab. Für diese wird jeweils festgelegt, welcher Ausdruck zurückgegeben wird.

Syntax SWITCH

SWITCH <Returntype>( <ComparisonValue> WHEN <Const1> THEN <Expression1>
                                      [WHEN <Const2> THEN <Expression2>]
                                       ...
                                      [ELSE <ExpressionN>] )

Beispiel

    DATA(result) = switch tt_demo( sy-datum WHEN '20220929' THEN VALUE #( ( amount = 10 curr = 'EUR'  ) )
                                            WHEN '20220930' THEN VALUE #( ( amount = 11 curr = 'EUR'  ) )
                                            ELSE                 VALUE #( ( amount = 9 curr = 'USD' ) ) ).
    out->write( result ).

Falls kein Vergleich zutrifft und kein ELSE Ausdruck festgelegt wurde, wird der Initiale Wert des Rückgabetyps zurückgegeben.

(C) Brandeis Consulting.

Fallunterscheidungen mit dem COND Operator

Der COND Operator entspricht einem "Searched CASE" bzw. CASE WHEN- Ausdruck in SQL. Voneinander unabhängige Bedingungen werden geprüft. Die erste, die zu TRUE ausgewertet wird, bestimmt den Rückgabewert. Falls keine zutrifft, wird der ELSE Wert zurückgegeben. Ist dieser nicht definiert, wird ein initialer Wert zurückgegeben.

Syntax COND

COND <Returntype>(  WHEN <Condition1> THEN <Expression1>
                   [WHEN <Condition2> THEN <Expression2>]
                   ...
                   [ELSE <ExpressionN>] )

Beispiel

    DATA(result) = COND tt_demo( WHEN sy-datum = '20220929' THEN VALUE #( ( amount = 10 curr = 'EUR'  ) )
                                 WHEN sy-langu = 'D'        THEN VALUE #( ( amount = 11 curr = 'EUR'  ) )
                                 ELSE                            VALUE #( ( amount = 9 curr = 'USD' ) ) ).
(C) Brandeis Consulting.

Filtern von internen Tabellen mit FILTER (1/2)

Mit dem FILTER Operator können interne Tabellen auf Basis einer anderen internen Tabelle mittels Filterung erzeugt werden. Das passiert entweder mit einer einfachen WHERE-Klausel oder mit einer anderen Tabelle.

SAP Dokumentation einfaches Filtern

Beim Filtern mit FILTER müssen die Filterkriterien in einem sortierten oder gehashten Schlüssel sein. Ansonsten gibt es einen Syntaxfehler.

Beispiel Filter mit WHERE

    DATA lt_data TYPE sorted TABLE OF I_CountryText WITH UNIQUE KEY language  country.
    SELECT * FROM i_countrytext INTO TABLE @lt_data.

    out->write( filter #( lt_data where language = 'D' ) ).
(C) Brandeis Consulting.

Filtern von internen Tabellen mit FILTER (2/2)

Beispiel FILTER mit anderer Tabelle

Oder es wird anhand einer anderen internen Tabelle gefiltert. Das entspricht einem INNER JOIN in SQL.


    DATA lt_data TYPE sorted TABLE OF I_CountryText WITH UNIQUE KEY country.
    DATA(lt_filter) = VALUE tt_demo( ( country = 'DE' )
                                   ( country = 'US' ) ).
    SELECT * FROM i_countrytext WHERE language = 'D' INTO TABLE @lt_data.

    DATA(lt_new) = filter #( lt_data in lt_filter where country = country  ).

    out->write( lt_new ).

(C) Brandeis Consulting.

REDUCE Ausdrücke

Mit REDUCE wird eine interne Tabelle auf einen einzelnen Wert reduziert. Dabei kann dieser Wert ebenfalls wieder eine Tabelle sein.

    SELECT  summary
      FROM zbc_tasks
      INTO TABLE @DATA(lt_tasks)
      UP TO 10 ROWS.

    DATA(lv_result) = REDUCE string( INIT r  TYPE string
                                     FOR line IN lt_tasks
                                     NEXT
                                      r &&= line-summary(1)
                                      ).
    out->write( lv_result ).
    out->write( lt_tasks ).
(C) Brandeis Consulting.

Syntax des REDUCE Operators

Einfachster Fall

REDUCE <Returntype>( INIT <Variable Declarations>
                     FOR <FOR-Expression>
                     NEXT <Result Construction> ). 

Ausbaustufen

  • Vor der Variablen Deklaration können mit LET Hilfsvariablen deklariert werden.
  • Es können mehrere Variablen deklariert werden. Die erste Variable wird als Ergebnis des REDUCE Ausdrucks verwendet und muss in den Returntype konvertierbar sein.
  • Es kann mit mehreren FORauch über mehrere interne Tabellen geschleift werden.
  • Im NEXT Bereich können auch wiederum interne Tabellen konstruiert werden
(C) Brandeis Consulting.

Literaturhinweise & Beispiele zu REDUCE:

(C) Brandeis Consulting.

Clean Code Empfehlungen

  • Keine zu komplexen Dinge umsetzen - Die Operation sollte überschaubar bleiben
  • Interne Variablen kurz halten
  • Bedenke: Für den Debugger ist das ein einzelner Schritt
  • Die Typkonvertierung mit CONV kann gerade bei Methodenaufrufen, wo eine strenge Typprüfung vorgenommen wird, sehr praktisch sein. Andererseits hebelt man damit die Vorteile dieser Prüfung aus. Damit verlagert man potenzielle Fehler vom Entwicklungszeitpunkt (Syntaxfehler) zum Ausführungszeitpunkt (Dump).
(C) Brandeis Consulting.