ABAP Trapdoors: Morphing Method Parameters

This is a repost of an article published on the SAP Community Network.


During our basic programming lessons, we all learned how to control the flow of data throughout a series of method calls and why that is important. Hopefully, you've come to dislike FORM foo USING bar as much as I do and substitute it with METHOD foo IMPORTING x EXPORTING x CHANGING z, omitting the CHANGING part wherever possible. This way, you can always tell which parameters you have to supply the method with and which parameters only yield output values. You can also rely on the value of the exporting parameter being exclusively determined by the method call, unaffected of other actions - or can you? Let's take a closer look:

*----------------------------------------------------------------------*
 
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.
 
*----------------------------------------------------------------------*

The result is most definitely boring:

This one goes out to the one...

Now, let's try this in a different way. We need to be able to pass multiple lines of text for some reason, so we'll just use a table of strings.

*----------------------------------------------------------------------*
 
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.
 
*----------------------------------------------------------------------*

Ready for a test run?

first line
I Think I'll Disappear Now
last line

Ahem. Perhaps not quite what we expected. We can even take this one step further. When an exception occurs, we usually expect that the method we just called has failed to perform its assigned function and not have any secondary side effects. We particularly don't want it to return data that has been half-processed. So how about this:

*----------------------------------------------------------------------*
 
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.
 
*----------------------------------------------------------------------*

After this introduction, you've probably expected the outcome:

Whoops, silly me!
Right Between
The Eyes

So what is going on here? These are EXPORTING parameters, not CHANGING parameters, so why do the methods actually add data to the table instead of overwriting it? The answer is actually rather simple - because ABAP uses call by reference by default. This means that the method receives a reference to the original variable passed by the caller and therefore operates on this variable. This also implies that whatever content the variables passed as EXPORTING parameters have when the method is called are passed on to the method implementation. By the way, the same thing happens when you pass structures to the method and don't fill all fields of the structure during the execution of the method - you might end up with left-over data.

The obvious solution is to just replace EXPORTING et_data with EXPORTING VALUE(et_data) in the examples above to switch from call by reference to call by value. This will cause the method to operate on its own private variable that is initialized before the method is executed. The results are then copied back to the variable when the method exits normally. If an exception occurs, the original data is not touched at all.

Unfortunately, this also causes severe performance degradation, especially where large tables are involved. It's a clumsy workaround that forces the system to copy around potentially massive amounts of data - sometimes that's the only option, but using this as a default is not advisable. Most of the time, you just have to keep in mind that it's a good idea to REFRESH the exporting tables at the beginning of your method implementation.

The fact that ABAP defaults to call by reference parameter passing also explains why the following rather irritating code is possible:

*----------------------------------------------------------------------*
 
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( ).
 
*----------------------------------------------------------------------*

The result:

A Standard Chorus Line.
Whoops, where did that input value come from?

This is surprising at a first glance because the constructor of lcl_super checks the importing parameter twice, and between these checks the parameter value changes mysteriously. Again, this happens because the constructor is passed a reference to its own attribute, and by changing gt_text, it also changes its own input parameter implicitly. Something like this was spotted by a colleague of reader Peter Inotai in the famous class CL_GUI_ALV_GRID. From my point of view, this is rather pointless and can lead to really confusing behavior. Moreover, if happen to work on a shared codebase in a larger project and you rely on this behavior, it's probably only a question of time before someone who is not aware of the implications and/or your intentions breaks something.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer