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.