Diesen Artikel habe ich ursprünglich im SAP Community Network veröffentlicht.
Während unserer grundlegenden Programmierkurse haben wir alle gelernt, wie man den Datenfluss durch eine Reihe von
Methodenaufrufen steuert und warum das wichtig ist. Hoffentlich mögen Sie FORM foo USING bar
genauso wenig wie ich und
ersetzen es durch METHOD foo IMPORTING x EXPORTING x CHANGING z
, wobei Sie den Teil CHANGING
nach Möglichkeit
weglassen. Auf diese Weise können Sie immer erkennen, welche Parameter Sie der Methode bereitstellen müssen und welche
Parameter nur Ausgabewerte liefern. Sie können sich auch darauf verlassen, dass der Wert des Exportparameters
ausschließlich durch den Methodenaufruf bestimmt wird, unabhängig von anderen Aktionen – oder etwa nicht? Schauen wir
uns das genauer an:
CLASS lcl_myclass DEFINITION.
PUBLIC SECTION.
METHODS get_text EXPORTING e_data TYPE string.
ENDCLASS.
CLASS lcl_myclass IMPLEMENTATION.
METHOD get_text.
e_data = 'This one goes out to the one...'.
ENDMETHOD.
ENDCLASS.
DATA: gr_myclass TYPE REF TO lcl_myclass,
g_string TYPE string.
CREATE OBJECT gr_myclass.
g_string = 'Nothing to fear'.
CALL METHOD gr_myclass->get_text
IMPORTING
e_data = g_string.
WRITE: / g_string.
Das Ergebnis ist definitiv langweilig:
This one goes out to the one…
Versuchen wir es jetzt auf eine andere Art und Weise. Wir müssen aus irgendeinem Grund mehrere Textzeilen übergeben können, also verwenden wir einfach eine Tabelle mit Zeichenketten.
CLASS lcl_myclass DEFINITION.
PUBLIC SECTION.
METHODS get_text EXPORTING et_data TYPE string_table.
ENDCLASS.
CLASS lcl_myclass IMPLEMENTATION.
METHOD get_text.
INSERT 'first line' INTO et_data INDEX 1.
APPEND 'last line' TO et_data.
ENDMETHOD.
ENDCLASS.
DATA: gr_myclass TYPE REF TO lcl_myclass,
gt_strings TYPE string_table.
FIELD-SYMBOLS: <g_string> TYPE string.
CREATE OBJECT gr_myclass.
APPEND 'I Think I'll Disappear Now' TO gt_strings.
CALL METHOD gr_myclass->get_text
IMPORTING
et_data = gt_strings.
LOOP AT gt_strings ASSIGNING <g_string>.
WRITE: / sy-tabix, <g_string>.
ENDLOOP.
Bereit für einen Testlauf?
first line
I Think I’ll Disappear Now
last line
Ähm. Vielleicht nicht ganz das, was wir erwartet haben. Wir können sogar noch einen Schritt weiter gehen. Wenn eine Ausnahme auftritt, erwarten wir normalerweise, dass die gerade aufgerufene Methode die ihr zugewiesene Funktion nicht ausführen konnte und keine sekundären Nebenwirkungen hat. Wir möchten insbesondere nicht, dass sie Daten zurückgibt, die nur zur Hälfte verarbeitet wurden. Wie wäre es also damit:
CLASS lcl_myclass DEFINITION.
PUBLIC SECTION.
METHODS get_text EXPORTING et_data TYPE string_table RAISING cx_no_such_entry.
ENDCLASS.
CLASS lcl_myclass IMPLEMENTATION.
METHOD get_text.
INSERT 'Right Between' INTO et_data INDEX 1.
RAISE EXCEPTION TYPE cx_no_such_entry.
APPEND 'Here And Nowhere' TO et_data.
ENDMETHOD.
ENDCLASS.
DATA: gr_myclass TYPE REF TO lcl_myclass,
gt_strings TYPE string_table.
FIELD-SYMBOLS: <g_string> TYPE string.
CREATE OBJECT gr_myclass.
APPEND 'The Eyes' TO gt_strings.
TRY.
CALL METHOD gr_myclass->get_text
IMPORTING
et_data = gt_strings.
CATCH cx_no_such_entry.
WRITE: / 'Whoops, silly me!'.
ENDTRY.
LOOP AT gt_strings ASSIGNING <g_string>.
WRITE: / sy-tabix, <g_string>.
ENDLOOP.
Nach dieser Einführung haben Sie wahrscheinlich das Ergebnis erwartet:
Whoops, silly me!
Right Between
The Eyes
Was passiert hier also? Dies sind EXPORTING
-Parameter, keine CHANGING
-Parameter. Warum fügen die Methoden also Daten
zur Tabelle hinzu, anstatt sie zu überschreiben? Die Antwort ist eigentlich ganz einfach: Weil ABAP standardmäßig
Referenzübergabe verwendet. Das bedeutet, dass die Methode eine Referenz auf die ursprüngliche Variable erhält, die vom
Aufrufer übergeben wurde, und daher mit dieser Variable arbeitet. Dies bedeutet auch, dass der Inhalt, den die
Variablen, die als EXPORTING
-Parameter übergeben wurden, beim Aufruf der Methode haben, an die
Methodenimplementierung weitergegeben wird. Dasselbe passiert übrigens, wenn Sie Strukturen an die Methode übergeben
und nicht alle Felder der Struktur während der Ausführung der Methode ausfüllen – dann könnten Sie am Ende mit übrig
gebliebenen Daten dastehen.
Die naheliegende Lösung besteht darin, in den obigen Beispielen einfach EXPORTING et_data
durch EXPORTING VALUE(et_data)
zu ersetzen, um von Referenz- auf Werteübergabe zu wechseln. Dadurch wird die Methode auf ihrer eigene
private Variable arbeiten, die vor der Ausführung der Methode initialisiert wird. Die Ergebnisse werden dann beim
normalen Beenden der Methode wieder in die Empfänger-Variable kopiert. Wenn eine Ausnahme auftritt, werden die
Originaldaten überhaupt nicht berührt.
Leider führt dies auch zu einer erheblichen Leistungsminderung, insbesondere bei großen Tabellen. Es handelt sich um
eine ungeschickte Behelfslösung, die das System dazu zwingt, potenziell riesige Datenmengen zu kopieren – manchmal ist
das die einzige Option, aber es ist nicht ratsam, dies als Standard zu verwenden. Meistens muss man nur daran denken,
dass es eine gute Idee ist, die Exporttabellen zu Beginn der Methodenimplementierung mit REFRESH
zu initialisieren.
Die Tatsache, dass ABAP standardmäßig die Übergabe von Parametern per Referenz vorsieht, erklärt auch, warum der folgende, eher irritierende Code möglich ist:
CLASS lcl_super DEFINITION.
PUBLIC SECTION.
METHODS constructor IMPORTING it_text TYPE string_table.
METHODS write_text.
PROTECTED SECTION.
DATA gt_text TYPE string_table.
ENDCLASS.
CLASS lcl_super IMPLEMENTATION.
METHOD constructor.
IF it_text IS NOT INITIAL.
APPEND 'Message In A Bottle' TO gt_text.
ENDIF.
APPEND 'A Standard Chorus Line.' TO gt_text.
IF it_text IS NOT INITIAL.
APPEND 'Whoops, where did that input value come from?' TO gt_text.
ENDIF.
ENDMETHOD.
METHOD write_text.
FIELD-SYMBOLS: <l_line> TYPE string.
LOOP AT gt_text ASSIGNING <l_line>.
WRITE: / sy-tabix, <l_line>.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
CLASS lcl_sub DEFINITION INHERITING FROM lcl_super.
PUBLIC SECTION.
METHODS constructor.
ENDCLASS.
CLASS lcl_sub IMPLEMENTATION.
METHOD constructor.
* set break point here to verify that gt_text is really empty
CALL METHOD super->constructor
EXPORTING
it_text = gt_text.
ENDMETHOD.
ENDCLASS.
DATA: gr_instance TYPE REF TO lcl_sub.
START-OF-SELECTION.
CREATE OBJECT gr_instance.
gr_instance->write_text( ).
Das Ergebnis:
A Standard Chorus Line.
Whoops, where did that input value come from?
Das ist auf den ersten Blick überraschend, da der Konstruktor von lcl_super
den Importparameter zweimal überprüft und
sich der Parameterwert zwischen diesen Überprüfungen auf mysteriöse Weise ändert. Dies geschieht wiederum, weil dem
Konstruktor eine Referenz auf sein eigenes Attribut übergeben wird und durch die Änderung von gt_text
auch sein
eigener Eingabeparameter implizit geändert wird. Etwas Ähnliches wurde von einem Kollegen des Lesers Peter Inotai in der
berühmten Klasse CL_GUI_ALV_GRID
entdeckt. Meiner Meinung nach ist dies ziemlich sinnlos und kann zu wirklich
verwirrend Verhalten führen. Wenn man außerdem an einer gemeinsam genutzten Codebasis in einem größeren Projekt arbeitet
und sich auf dieses Verhalten verlässt, ist es wahrscheinlich nur eine Frage der Zeit, bis jemand, der sich der
Auswirkungen und/oder Ihrer Absichten nicht bewusst ist, etwas kaputt macht.