Eine Zusicherung, Sicherstellung oder Assertion (lateinisch/englisch für Aussage, Behauptung) ist eine Aussage über den Zustand eines Computerprogramms oder einer elektronischen Schaltung. Mit Hilfe von Zusicherungen können logische Fehler im Programm oder Defekte in der umgebenden Hard- oder Software erkannt und das Programm kontrolliert beendet werden. Bei der Entwicklung elektronischer Schaltungen kann mittels Assertions die Einhaltung der Spezifikation in der Verifikationsphase überprüft werden. Des Weiteren können Assertions Informationen über den Grad der Testabdeckung während der Verifikation liefern.
Anwendung
Durch die Formulierung einer Zusicherung bringt der Entwickler eines Programms seine Überzeugung über bestimmte Bedingungen während der Laufzeit eines Programms zum Ausdruck und lässt sie Teil des Programms werden. Er trennt diese Überzeugungen von den normalen Laufzeitumständen ab und nimmt diese Bedingungen als stets wahr an. Abweichungen hiervon werden nicht regulär behandelt, damit die Vielzahl möglicher Fälle nicht die Lösung des Problems vereitelt, denn natürlich kann es während der Laufzeit eines Programms dazu kommen, dass 2+2=4 einmal nicht gilt, z. B. weil Variablen durch Programmfehler im Betriebssystem überschrieben wurden.
Damit unterscheiden sich Zusicherungen von der klassischen Fehlerkontrolle durch Kontrollstrukturen oder Ausnahmen (Exceptions), die einen Fehlerfall als mögliches Ergebnis einschließen. In einigen Programmiersprachen wurden Zusicherungen auf Sprachebene eingebaut, häufig werden sie als Sonderform der Ausnahmen verwirklicht.
Fehlerbehandlungen sollten für Fehlerzustände geschrieben werden, welche man erwartet; Zusicherungen für Fehlerzustände, die niemals auftreten sollten.
Geschichte
Eingeführt wurde der Begriff assertion von Robert Floyd 1967 in seinem Artikel Assigning Meanings to Programs. Er schlug eine Methode vor, mit der man die Korrektheit von Flussdiagrammen beweisen konnte, indem man jedes Element des Flussdiagramms mit einer Zusicherung versieht. Floyd gab Regeln an, nach denen die Zusicherungen bestimmt werden konnten. Tony Hoare entwickelte diese Methode zum Hoare-Kalkül für prozedurale Programmiersprachen weiter. Im Hoare-Kalkül wird eine Zusicherung, die vor einer Anweisung steht, Vorbedingung (englisch precondition), eine Zusicherung nach einer Anweisung Nachbedingung (englisch postcondition) genannt. Eine Zusicherung, die bei jedem Schleifendurchlauf erfüllt sein muss, heißt Invariante.
Niklaus Wirth benutzte Zusicherungen zur Definition der Semantik von Pascal und schlug vor, dass Programmierer in ihre Programme Kommentare mit Zusicherungen schreiben sollten. Aus diesem Grund sind Kommentare in Pascal mit geschweiften Klammern {…} umgeben, eine Syntax, die Hoare in seinem Kalkül für Zusicherungen verwendet hatte.
In Borland Delphi wurde die Idee übernommen und ist als System-Funktion assert
eingebaut. In der Programmiersprache Java steht ab Version 1.4 das Schlüsselwort assert
zur Verfügung.
Die zur Entwicklung elektronischer Schaltungen eingesetzten Hardwarebeschreibungssprachen VHDL und SystemVerilog unterstützen Assertions. PSL ist eine eigenständige Beschreibungssprache für Assertions, die Modelle in VHDL, Verilog und SystemC unterstützt. Während der Verifikation wird vom Simulationswerkzeug erfasst, wie oft die Assertion ausgelöst wurde und wie oft die Zusicherung erfüllt oder verletzt wurde. Wurde die Assertion ausgelöst und die Zusicherung nie verletzt, gilt die Schaltung als erfolgreich verifiziert. Wurde jedoch die Assertion während der Simulation nie ausgelöst, besteht eine mangelnde Testabdeckung und die Verifikationsumgebung muss erweitert werden.
Programmierpraxis
In der Programmiersprache C könnte eine Zusicherung in etwa so eingesetzt werden:
#include <assert.h>
/// diese Funktion liefert die Länge eines nullterminierten Strings
/// falls der übergebene auf die Adresse NULL verweist, wird das
/// Programm kontrolliert abgebrochen. (strlen prüft das nicht selbst)
int strlenChecked(char* s) {
assert(s != NULL);
return strlen(s);
}
In diesem Beispiel wird über die Einbindung der Header-Datei assert.h das Makro assert
verwendbar. Dieses Makro sorgt im Falle eines Fehlschlags für die Ausgabe einer Standardmeldung, in der die nicht erfüllte Bedingung zitiert wird und Dateiname und Zeilennummer hinzugefügt werden. Eine solche Meldung könnte dann so aussehen:
Assertion "s!=NULL" failed in file "C:\Projects\Sudoku\utils.c", line 9
Java kennt das Konzept der Zusicherungen ab Version 1.4. Hier wird allerdings das Programm nicht notwendigerweise beendet, sondern eine sogenannte Ausnahme (englisch exception) ausgelöst, die innerhalb des Programms weiterverarbeitet werden kann.
Ein einfaches Beispiel einer Assertion (hier in Java-Syntax) ist
int n = readInput();
n = n * n; //Quadrieren
assert n >= 0;
Mit dieser Assertion sagt der Programmierer „Ich bin mir sicher, dass nach dieser Stelle n größer gleich null ist“.
Bertrand Meyer hat die Idee von Zusicherungen in dem Programmierparadigma Design by contract verarbeitet und in der Programmiersprache Eiffel umgesetzt. Vorbedingungen werden durch require-Klauseln, Nachbedingungen durch ensure-Klauseln beschrieben. Für Klassen können Invarianten spezifiziert werden. Auch in Eiffel werden Ausnahmen ausgelöst, wenn eine Zusicherung nicht erfüllt ist.
Verwandte Techniken
Assertions entdecken Programmfehler zur Laufzeit beim Anwender, also erst, wenn es zu spät ist. Um Meldungen über "Interne Fehler" möglichst zu vermeiden, versucht man, durch geeignete Formulierung des Quelltextes logische Fehler bereits zur Kompilierzeit (durch den Compiler) in Form von Fehlern und Warnungen aufdecken zu lassen. Logische Fehler, die man auf diese Weise nicht finden kann, können häufig mittels Modultests aufgedeckt werden.
Umformulieren des Quelltextes
Indem Fallunterscheidungen auf ein Minimum reduziert werden, ist mancher Fehler nicht ausdrückbar. Dann ist er als logischer Fehler auch nicht möglich.
// Kurvenrichtung
public enum TURN { LEFT_TURN, RIGHT_TURN }
// Methode liefert den Text zu einer Kurvenrichtung
public String getTurnText(TURN turn) {
switch(turn) {
case LEFT_TURN: return "Linkskurve";
case RIGHT_TURN: return "Rechtskurve";
default: assert false: "Es gibt nur Links- oder Rechtskurven"; // Kann nicht auftreten
}
}
Nehmen wir an, es gäbe nur die zwei Fälle (das ist in der Tat häufig so), dann könnten wir einen einfachen Wahrheitswert verwenden, anstelle einer spezielleren Kodierung:
// Methode liefert den Text zu einer Kurvenrichtung
public String getTurnText(boolean isLeftTurn) {
if (isLeftTurn) return "Linkskurve";
return "Rechtskurve";
}
In vielen Fällen wird so wie in diesem Beispiel dadurch der Code weniger explizit und somit unverständlicher.
Zusicherungen zur Kompilierzeit
Während die oben beschriebenen Assertionen zur Laufzeit des Programms geprüft werden, gibt es in C++ die Möglichkeit, Bedingungen auch schon beim Übersetzen des Programms durch den Compiler zu überprüfen. Es können nur Bedingungen nachgeprüft werden, die zur Übersetzungszeit bekannt sind, z. B. sizeof(int) == 4
. Schlägt ein Test fehl, lässt sich das Programm nicht übersetzen:
#if sizeof(int) != 4
#error "unerwartete int-Größe"
#endif
Früher hing das stark von der Funktionalität des jeweiligen Compilers ab und manche Formulierungen waren gewöhnungsbedürftig:
/// Die Korrektheit unserer Implementierung hängt davon ab,
/// dass ein int 4 Bytes groß ist. Falls dies nicht gilt,
/// bricht der Compiler mit der Meldung ab, dass
/// Arrays mindestens ein Element haben müssen:
void validIntSize(void) {
int valid[sizeof(int)==4];
}
Mit C11 bzw. C++11 wurden zur Lösung dieses Problems je die Schlüsselworte _Static_assert bzw. static_assert (in C11 zusätzlich als Makro implementiert) eingeführt:
//in C als auch C++
void validIntSize(void) {
static_assert(sizeof(int)==4,
"implementation of validIntSize does not work with the int size of your compiler");
}
Zusicherungen in Modultests
Ein Bereich in dem ebenfalls Zusicherungen eine Rolle spielen, sind Modultests (u. a. Kernbestandteil des Extreme Programmings). Wann immer man am Quelltext Änderungen (z. B. Refactorings) vornimmt, z. B. um weitere Funktionen zu integrieren, führt man Tests auf Teilfunktionen (Modulen, also z. B. Funktionen und Prozeduren, Klassen) aus, um die bekannte (gewünschte) Funktionalität zu evaluieren.
Im Gegensatz zu assert wird bei einem Fehlschlag nicht das ganze Programm beendet, sondern nur der Test als gescheitert betrachtet und weitere Tests ausgeführt, um möglichst viele Fehler zu finden. Laufen alle Tests fehlerfrei, dann sollte davon ausgegangen werden können, dass die gewünschte Funktionalität besteht.
Von besonderer Bedeutung ist der Umstand, dass Modultests üblicherweise nicht im Produktivcode ausgeführt werden, sondern zur Entwicklungszeit. Das bedeutet, dass Assertions im fertigen Programm als Gegenpart zu betrachten sind.
Spezielle Techniken für Microsoft-Compiler
Neben Assert wird auch Verify verwendet. Verify führt alle Anweisungen aus, die in die Berechnung der Bedingung einfließen, gleichgültig, ob mit oder ohne Debug-Absicht kompiliert wurde. Die Überprüfung findet aber nur in der Debug-Variante statt, d. h. auch hier kann ein Programm in obskurer Weise scheitern, falls die Zusicherung nicht erfüllt ist.
// die Variable anzahl wird immer erhöht
Verify(++anzahl>2);
Assert_Valid wird eingesetzt, um Objekte auf ihre Gültigkeit zu testen. Dazu gibt es in der Basisklasse der Objekthierarchie die virtuelle Methode AssertValid()
. Die Methode sollte für jede konkrete Klasse überschrieben werden, um die internen Felder der Klasse auf Gültigkeit zu testen.
// Example for CObject::AssertValid.
void CAge::AssertValid() const
{
CObject::AssertValid();
ASSERT( m_years > 0 );
ASSERT( m_years < 105 );
}
Literatur
- Robert W. Floyd: Assigning meanings to programs. In: Proceedings of Symposia in Applied Mathematics, Volume 19, 1967, S. 19–32.
Weblinks
Einzelnachweise
- ↑ Steve McConnell: Code Complete. A Practical Handbook of Software Construction. Hrsg.: Microsoft Press. 2. Auflage. 2004, ISBN 978-0-7356-1967-8 (englisch).