Die Verwendung der Behauptung (assertions) bei der Entwicklung der Programme in MQL5

Sergey Eremin | 16 Mai, 2016

Einführung

Eine Behauptung — ist eine spezielle Konstruktion, die ermöglicht, die willkürlichen Annahmen in den beliebigen Stellen des Programms zu überprüfen. Sie werden typischerweise in Form von einem Code (meist als separate Funktion oder Makros) dargestellt. Dieser Code prüft einen wahren Wert eines bestimmten Ausdrucks. Wenn es scheint falsch zu sein, wird eine entsprechende Meldung angezeigt, und das Programm wird gestoppt, wenn diese Aktion in der Realisierung vorgesehen ist. Dementsprechend bedeutet es, wenn der Ausdruck wahr ist, dann funktioniert alles wie geplant — die Annahme ist korrekt erfüllt. Ansonsten können Sie sicher sein, dass der Fehler im Programm gefunden wurde und es wird berichtet.

Zum Beispiel, wenn ein bestimmter Wert X innerhalb des Programms unter keinen Umständen weniger als Null ist, dann kann die folgende Behauptung gemacht werden: "Ich behaupte, dass der Wert von X größer oder gleich Null ist". Wenn X weniger als Null ist, dann wird eine entsprechende Meldung angezeigt, und Programmierer kann das Programm einstellen.

Die Behauptungen sind besonders bei großen Projekten nützlich, deren Bestandteile mit der Zeit wieder verwendet oder modifiziert werden können.

Die Behauptungen sollten nur solche Situationen abdecken, die nicht während der normalen Arbeit des Programms auftreten sollen. In der Regel können Behauptungen nur auf den Entwicklungs- und Einrichtungs-Phasen des Programms angewendet werden, das heißt, sie sollten nicht in der finalen Version sein. Alle Behauptungen müssen bei der Kompilation der endgültigen Versionen entfernt werden. Dies wird in der Regel durch die bedingte Kompilation erreicht.


Beispiele für die Realisierung des Behauptungsmechanismus in MQL5

Die folgenden Funktionen werden in der Regel durch das Behauptungsmechanismus vorgesehen:

  1. Die Lieferung des Behauptungstextes, der zur Überprüfung gegeben wurde.
  2. Die Lieferung des Dateinamens mit dem Quellcode, in dem ein Fehler gefunden wurde.
  3. Die Lieferung des Namens und der Signatur einer Funktion oder einer Methode, bei der ein Fehler gefunden wurde.
  4. Die Lieferung einer Zeilennummer in einer Quelldatei, in der eine Behauptung überprüft wird.
  5. Die Lieferung einer beliebigen Nachricht, die beim Code-Schreiben vom Programmierer gegeben wird.
  6. Programmabbruch, falls Fehler gefunden werden.
  7. Die Möglichkeit, alle Behauptungen aus dem kompilierten Programm durch die bedingte Kompilation oder durch ein ähnliches Mechanismus auszuschließen.

Die Standard-Funktionen ermöglichen fast alle diese Möglichkeiten zu realisieren (abgesehen. vom Punkt 6. siehe weiter unten), und auch außer dem Mechanismus Makro und der bedingten Kompilation der MQL5 Sprache. Denn die zwei aus möglichen Varianten könnten auf folgende Weise aussehen:

Die Variante №1 (Soft-Version, das Programm wird nicht zwangsweise beendet)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
        }
#else
   #define assert(condition, message) ;
#endif 

Die Variante №2 (so harte Version, das Programm wird zwangsweise beendet)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
         double x[]; \
         ArrayResize(x, 0); \
         x[1] = 0.0; \
        }
#else 
   #define assert(condition, message) ;
#endif


Die Struktur der Makro assert

Zunächst wird der Identifikator DEBUG erklärt. Wenn dieser Identifikator erklärt bleibt, dann wird der Zweig #ifdef des bedingten Kompilations-Ausdrucks gültig sein, und zum Programm wird eine voll funktionsfähige Makro assert hinzugefügt. Andernfalls (der Zweig #else) wird zum Programm die Makro assert hinzugefügt, die keine Ausführung einer Operation durchführt.

Eine voll funktionsfähige Makro assert wird nach folgender Art und Weise gebaut. Zunächst wird ein eingehender Ausdruck condition überprüft. Wenn es falsch ist, dann wird eine Nachricht fullMessage gebildet und angezeigt. Die Nachricht fullMessage wird aus den folgenden Elementen aufgebaut:

  1. Der Text des Ausdrucks,der zur Überprüfung gegeben wurde (#condition).
  2. Der Dateiname mit einem Quellcode, aus dem die Makro (__FILE__) aufgerufen wurde.
  3. Die Signatur der Funktion oder Methode, aus der die Makro (__FUNCSIG__) aufgerufen wurde.
  4. Die Zeilennummer in einer Datei mit dem Quellcode, in dem ein Makro-Aufruf platziert wird (__LINE__).
  5. Die Nachricht, die an eine Makro übertragen wurde, wenn es nicht leer ist (message).

Nach der Lieferung der Meldung(Alert) wird in dem zweiten Makrotyp versucht, einen Wert zu einem nicht existierenden Array-Element zuzuweisen, dies führt zu einem Fehler bei der Ausführungszeit und bewirkt so, dass das Programm direkt abstürzt.

Dieses Verfahren, das Programm zu beenden hat Nebenwirkungen für die Indikatoren, die in ihren Unterfenster arbeiten: ihre Unterfenster bleiben im Terminal, und deshalb müssen sie manuell geschlossen werden. Außerdem können Artefakte in Form von nicht entfernten grafischen Objekten, globale Variablen des Terminals, Dateien, etc. sein, die während der Arbeit des Programms erstellt wurden, bis das abgestürzt ist. Wenn dieses Verhalten vollständig inakzeptabel ist, dann sollte die erste Makro verwendet werden.

Erläuterung. Im Moment des Schreibens dieses Artikels in MQL5 gab es kein Mechanismus, das Programm im Notfall auszumachen. Als Alternative rufen wir den Laufzeitfehler auf, der das Programm garantiert zum Absturz zwingt.

Dieses Makro kann in der separaten Datei assert.mqh platziert werden, die beispielsweise im <Datenordner> / MQL5 /Include ist. Diese Datei (Die Variante №2) ist zum Artikel hinzugefügt.

Unten wird der Code gezeigt, in dem das Beispiel für die Verwendung der Behauptung und deren Arbeitsergebnis ergibt.

Das Beispiel für die Verwendung der Makro assert im Code Expert Advisors

#include <assert.mqh>

int OnInit()
  {
   assert(0 > 1, "my message")   

   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {  
  }

void OnTick()
  {
  }

Hier finden Sie eine Behauptung, das bedeutet wörtlich «Ich bestätige, dass 0 größer als 1 ist». Die Behauptung ist offensichtlich falsch, was zur Lieferung einer Fehlermeldung führt:

Abb. 1. Das Beispiel der Behauptungsfunktionalität

in Abb. 1. Das Beispiel der Behauptungsfunktionalität


Allgemeine Prinzipien für die Verwendung der Behauptungen

Die Behauptung sollte für die Identifizierung der unvorhergesehenen Situationen im Programmlauf verwenden und für das Dokumentieren und die Kontrolle über die Ausführung der akzeptierten Annahmen. Zum Beispiel können Behauptungen bei der Überprüfung der folgenden Bedingungen verwendet werden:

  • Die Werte von Ein- und Ausgangsparametern, zusammen mit resultierenden Werten von Funktionen und Methoden liegen im erwarteten Bereich.

    Das Beispiel für die Überprüfung der Eingangs- und Ausgangswerte der Methode mit Behauptungen
    double CMyClass::SomeMethod(const double a)
      {
    //--- Die Überprüfung des Wertes vom Eingangsparameter
       assert(a>=10,"")
       assert(a<=100,"")
    
    //--- Die Berechnung des resultierenden Wertes
       double result=...;
    
    //--- Die Überprüfung des resultierenden Wertes
       assert(result>=0,"")
      
       return result;
      } 
    
    Dieses Beispiel nimmt an, dass der Eingangsparameter a nicht weniger als 10 sein kann und nicht mehr als 100. Darüber hinaus wird erwartet, dass der resultierende Wert nicht kleiner als Null sein kann.

  • Die Grenzen des Arrays werden innerhalb des erwarteten Bereichs liegen.

    Das Beispiel für die Überprüfung durch Behauptungen, dass die Arrays Grenzen innerhalb des erwarteten Bereichs liegen
    void CMyClass::SomeMethod(const string &incomingArray[])
      {
    //--- Überprüfen die Arrays Grenzen
       assert(ArraySize(incomingArray)>0,"")
       assert(ArraySize(incomingArray)<=10,"")
    
       ...
      }
    
    Dieses Beispiel nimmt an, dass das incomingArray Array mindestens ein Element enthalten kann, aber nicht mehr als zehn.

  • Der Deskriptor des erzeugten Objekts ist nicht nullwertig.

    Das Beispiel für die Überprüfung durch Behauptungen, dass der Deskriptor des erzeugten Objekts nicht nullwertig ist
    void OnTick()
      {
    //--- Erstellen wir das Objekt a 
       CMyClass *a=new CMyClass();
    
    //--- Einige Aktionen
       ...
       ...
       ...
    
    //--- Überprüfen, daß das Objekt a immer noch existiert
       assert(CheckPointer(a),"")
    
    //--- Löschen wir das Objekt a
       delete a;
      } 
    
    Dieses Beispiel nimmt an, dass am Ende der Ausführung OnTick das Objekt a immer noch existiert.

  • Ist der Divisor bei der Division nicht nullwertig.

    Das Beispiel für die Überprüfung durch Behauptungen, ob der Divisor nicht nullwertig ist
    void CMyClass::SomeMethod(const double a, const double b)
      {
    //--- Überprüfen ob b nicht Null ist
       assert(b!=0,"")
    
    //--- dividieren von a durch b 
       double c=a/b;
      
       ...  
       ...
       ...
      } 
    
    Dieses Beispiel nimmt an, dass der Eingangsparameter b, der von a dividieren wird, ist nicht 0.

Es gibt sicherlich mehr Arten von Bedingungen, die mit Behauptungen überprüft werden sollen, und in jedem Fall sind sie absolut einzigartig. Einige davon sind oben gezeigt.

Verwenden Sie Behauptungen für die Überprüfung der Vor- und Nachbedingungen. Es gibt einen Ansatz für Software-Design und Entwicklung namens «Design by Contract». Nach diesem Ansatz schließt jede Funktion, Methode und Klasse einen Vertrag mit dem restlichen Teil des Programms durch die Vor- und Nachbedingungen.

Voraussetzungen — sind Vereinbarung, die einen Client-Code, der die Methode oder Klasse aufruft, führen vor dem Aufruf der Methode oder der Erstellung des Objektes aus. Mit anderen Worten, wenn angenommen wird, dass eine Methode einen bestimmten Parameter mit Werten größer als 10 nehmen sollte, dann muss sich der Programmierer darum kümmern, damit der Code, der die Methode aufruft, unter keinen Umständen einen Wert von weniger als oder gleich 10 auf diesen Parameter überträgt.

Nachbedingungen — sind Vereinbarungen, die eine Methode oder Klasse bei der Beendigung ihrer Arbeit ausführen. So erwartet es, wenn die Methode die Werte unter 100 nicht zurückliefert, dann muss sich der Programmierer darum kümmern, damit der Rückgabewert nie mehr oder gleich 100 wäre.

Es ist sehr bequem Vor- und Nachbedingungen zu dokumentieren, sowie deren Einhaltung bei der Entwicklung und Debugging-Phasen des Programms zu kontrollieren. Im Vergleich mit normalen Kommentaren werden Behauptungen nicht nur einfach die Erwartungen erklären, sondern werden auch ständig deren Erfüllung kontrollieren. Das Beispiel der Verwendung von Behauptungen zur Überprüfung und Dokumentation der Vor- und Nachbedingungen wurde oben gezeigt. «Das Beispiel für die Überprüfung der Eingangs- und Ausgangswerte der Methode mit Behauptungen».

Wenn es eine Gelegenheit gibt, verwenden Sie Behauptungen für die Überprüfung der Programmierfehler, das heißt, Fehler, die nicht durch äußere Faktoren beeinflusst werden. Von daher ist es für die Fehler, die von äußeren Faktoren verursacht werden können, und die vom Programmierer unabhängig sind, sollten sie nicht verwendet werden. Also, es ist keine gute Idee, Behauptungen für die Überprüfung der Richtigkeit der Trades Eröffnung, der Verfügbarkeit der History in einem bestimmten gewünschten Zeitraum usw. Für solche Fehler passt besser ihre Erarbeitung und Protokollierung.

Vermeiden Sie einen ausführbaren Code in Behauptungen zu platzieren. Da alle Behauptungen bei der Programms-Kompilation der endgültigen Version gelöscht werden können, sollten sie das Verhalten des Programms nicht beeinflussen. Zum Beispiel kommt dieses Problem oft, weil ein Aufruf einer Funktion oder Methode innerhalb des assert durchgeführt wird.

Die Behauptungen, die das Verhalten des Programms nach dem Deaktivieren aller Behauptungen beeinflussen können

void OnTick()

  {
   CMyClass someObject;

//--- einige Berechnungen für Richtigkeit zu überprüfen
   assert(someObject.IsSomeCalculationsAreCorrect(),"")
  
   ...
   ...
   ...
  }

In diesem Fall sollten Sie die Funktion aufrufen, bevor die Behauptung platziert wird, sein Ergebnis in einer bestimmten Statusvariable speichern und dann in der Behauptung überprüfen:

Die Behauptungen, die das Verhalten des Programms nach dem Deaktivieren aller Behauptungen beeinflussen können

void OnTick()
  {
   CMyClass someObject;

//---  einige Berechnungen für Richtigkeit zu überprüfen
   bool isSomeCalculationsAreCorrect = someObject.IsSomeCalculationsAreCorrect();
   assert(isSomeCalculationsAreCorrect,"")
  
   ...
   ...
   ...
  }

Verwechseln Sie nicht Behauptungen mit der erwarteten Fehlerverarbeitung. Die Behauptungen werden für die Suche nach Fehlern in Programmen bei der Entwicklung und Debugging-Phasen (Suche nach Programmierungsfehler) verwendet und die Verarbeitung der erwarteten Fehler wird für den ununterbrochen Ablauf der freigegebenen Version des Programms (idealerweise sind es die Fehler, die keine Programmierungsfehler sind) verwendet. Die Behauptung sollte den Fehler gar nicht verarbeiten, stattdessen sollten sie einfach schreien: "Hey, Kumpel, du hast einen Fehler hier"!».

Zum Beispiel in dem Fall, wenn eine Methode der Klasse einen Wert mehr als 10 zum Übergeben fordert, und der Programmierer versucht ihr 8 zu geben, dann ist das ein offensichtlicher Fehler des Programmierers und er sollte darüber mit einer Behauptung gewarnt werden:

Das Beispiel des Aufrufs für eine Methode mit einem nicht akzeptablen Wert des Eingangsparameters (Behauptungen werden zur Überprüfung des Parameterwertes verwendet)

void CMyClass::SomeMethod(const double a)

  {
//--- Überprüfen, was mehr als 10 ist
   assert(a>10,"")
  
   ...
   ...
   ...
  }

void OnTick()
  {
   CMyClass someObject;

   someObject.SomeMethod(8);
  
   ...
   ...
   ...
  }

Nun, wenn der Programmierer einen Code mit dem Übergeben des Wertes 8 ausgeführt hat, informiert ihn das Programm eindeutig: "Ich behaupte, dass Werte weniger oder gleich 10 auf diese Methode nicht übergeben werden dürfen».

Abb. 2. Das Ergebnis des Aufrufs für eine Methode mit einem nicht akzeptablen Wert des Eingangsparameters (Behauptungen werden zur Überprüfung des Parameterwertes verwendet)

in Abb. 2. Das Ergebnis des Aufrufs für eine Methode mit einem nicht akzeptablen Wert des Eingangsparameters (Behauptungen werden zur Überprüfung des Parameterwertes verwendet)

Nach einer solchen Nachricht kann der Programmierer ziemlich schnell seinen Fehler korrigieren.

Wenn es andererseits für die Arbeit des Programms notwendig ist, dass die History eines Instruments mehr als 1000 bars wäre, dann dürfte die umgekehrte Situation nicht als Programmierer-Fehler angesehen werden, da die zur Verfügung stehende History-Volumen nicht von ihm abhängig ist. Die Situation, in der es weniger als 1000 Bars zur Verfügung stehen, sollte logischerweise als ein erwarteter Fehler verarbeitet werden:

Das Beispiel für eine solche Situation, wenn die verfügbare History für ein Instrument kleiner ist als erforderlich (Für diese Situation wird die Fehlerverarbeitung verwendet)

void OnTick()
  {
   if(Bars(Symbol(),Period())<1000)
     {
      Comment("Nicht genügende Hystory für die korrekte Arbeit des Programms");
      return;
     }
  }

Für maximale Stabilität der endgültigen Version des Programms überprüfen Sie bitte Erwartungen mit Behauptungen und dann machen Sie trotzdem Fehlerverarbeitung:

Das Beispiel der allgemeinen Verwendung der Behauptungen und Fehlerverarbeitung

double CMyClass::SomeMethod(const double a)
  {
//--- Die Überprüfung des Wertes vom Eingangsparameter mit der Behauptung
   assert(a>=10,"")
   assert(a<=100,"")
  
//--- Die Überprüfung des Wertes vom Eingangsparameter und ihn korrigieren, falls es notwendig ist
   double aValue = a;

   if(aValue<10)
     {
      aValue = 10;
     }
   else if(aValue>100)
     {
      aValue = 100;
     }

//--- Berechnung des resultierenden Wertes
   double result=...;

//--- Die Überprüfung des resultierenden Wertes
   assert(result>=0,"")

//--- Die Überprüfung des resultierenden Wertes und ihn korrigieren, falls es notwendig ist
   if(result<0)
     {
      result = 0;
     }

   return result;
  } 


So helfen Behauptungen eine bestimmte Anzahl von Fehlern zu empfangen, bevor die endgültige Version veröffentlicht wird. Die Verarbeitung der Fehler in der endgültigen Version wird dem Programm ermöglichen, auch mit diesen Fehlern richtig zu arbeiten, die auf den Entwicklungs- und Debugging-Phasen nicht gefunden werden konnten.

Es gibt mehrere Methoden, Fehler zu verarbeiten: von der Korrektur der falschen Werten (wie im Beispiel oben) bis zu einem vollständigen Stopp einer Operation. Allerdings wächst ihre Betrachtung weit aus diesem Artikel.

Es wird auch angenommen, wenn das Programm auf einen Fehler des Programmierers mit einem korrekten Krachen reagiert und weist auf das Problem hin, dann sind Behauptungen in diesem Fall restlich. Beispielsweise bei der Teilung durch Null in MQL5 beendet das Programm seine Ausführung und zeigt die entsprechende Nachricht in dem Protokoll. Grundsätzlich hilft ein solcher Ansatz, Probleme zu finden, besser als es Behauptungen tun. Allerdings erlauben Behauptungen den Code mit den wichtigen Informationen über Annahmen zu ergänzen, die dadurch mehr bemerkbar (im Falle einer Verletzung) und aktueller werden als klassische Kommentare im Quellcode, und das kann wesentlich in der weiteren Unterstützung und der Entwicklung des Codes helfen.


Fazit

In diesem Artikel wurde der Behauptungsmechanismus betrachtet, wurde ein Beispiel für seine Realisierung in MQL5 dargestellt und es bietet auch allgemeine Empfehlungen über seine Anwendung an. Die richtig angewendeten Behauptungen ermöglichen die Software-Entwicklung und Debugging-Phasen erheblich zu vereinfachen.

Bitte denken Sie daran, dass Behauptungen zunächst für die Suche nach Programmierungsfehlern eingerichtet sind (Fehler vom Programmierer), und für die Fehler, die vom Programmierer unabhängig sind. Die endgültige Version des Programms enthält keine Behauptungen. Für mögliche Fehler, die vom Programmierer unabhängig sind, ist es am besten, die Fehlerverarbeitung zu verwenden.

Der Behauptungsmechanismus ist eng mit der Frage verbunden, wie man den Software testen kann. Allerdings sind die Fehlerverarbeitung und Software-Testen große Themen, welche die zusätzliche Aufmerksamkeit verdienen und sollten in anderen Artikeln betrachtet werden.