Bausteine

Die Überlegung, eine Programmiersprache in Bausteine zu unterteilen folgt aus zwei Gründen, die jeder für sich diese Betrachtung verlangen. Als wichtigerer Grund ist die Orthogonalität der Sprache genannt. Je weniger Bausteine verwendet werden, desto weniger Lernaufwand ist erforderlich. Je mehr Funktionalität zusammengelegt werden kann, desto weniger Sonderfälle sind zu vermitteln. Sonderfälle sind damit die zweite Begründung für die nähere Betrachtung: Weniger Sonderfälle bedeuten weniger Implementierungsaufwand.

Jede Programmiersprache benutzt diese Bausteine, allerdings nicht zwangsweise eine vollständige Implementierung der Bausteine oder eine alle Bausteine zusammen.

Welche Bausteine sind zu erwarten?

Die Frage nach den Bausteinen ergibt unbewusst beim Lehren von Programmiersprachen. Was muss ein Einsteiger wissen, um ein sein erstes Programm zu erstellen?

Konstrukt

Als Konstrukt werden einfache Befehle der Sprache bezeichnet, zum Beispiel „print“ oder „if“. Konstrukte nehmen - ähnlich wie der Assemblerbefehl - Einfluss auf das Programm ohne selber ein Ergebnis zurückzuliefern. Konstrukte sind, abgesehen von der leeren Anweisung („NOP“, No Operation in Assembler), von weiteren Daten abhängig. Dabei müssen Daten nicht zwangsweise übergeben werden, sie können auch impliziert durch die Sprache gegeben werden.

Konstanten

Konstanten sind fest definierte, eindeutige und unveränderliche Werte. Beispiele sind Zahlen ( 1, 2, 3 ), Fließkommazahlen ( 1.2, 3.3 ) oder Zeichenketten(„Hallo Welt“).

Symbole/Namen

Viele Begriffe werden bei Programmiersprachen nicht als Konstante verwendet, sondern als Symbol. Dazu gehören die sogenannten „Magic Numbers“, wo ein Begriff direkt einer Konstanten zugeordnet wird. Symbole werden ebenfalls benutzt, um wiederholt benutzte Bausteine benennen zu können. Der 'Symbol'-Baustein erhält je nach Verwendungszweck unterschiedliche Namen: Bezeichner, Funktionsname, Variablenname, Atom…

Funktionen

Unveränderliche Code-Abschnitte werden als Prozeduren, Funktionen (falls sie Rückgabewerte besitzen) oder Methoden (falls sie einer Klasse zugeordnet sind) bezeichnet. Diesen Unterprogrammen können in der Regel Parameter zugewiesen werden und sie haben einen Rückgabewert. Im Falle einer Prozedur, existiert kein Rückgabewert, der sich für eine weitere Verrechnung eignet: Der Rückgabetyp ist void. Funktionen werden in den gängigen Programmiersprachen (C, Pascal, Java) aufgerufen, in denen ihr Name gefolgt von den Übergabe-Parametern geschrieben wird:

printf( "Hallo %s\n", "Welt" );

Kontexte

Kontexte sind ein zur Zeit eher versteckter Baustein einer Programmiersprache, der aufgrund der gewünschten Anforderungen jedoch stärker in den Vordergrund geschoben werden muss. Ein Kontext beschreibt den Zustand, in dem sich eine Programmiersprache an einer bestimmten Stelle des Quelltextes befindet und wie Anweisungen syntaktisch und semantisch verstanden werden. Innerhalb einer bestimmten Programmiersprache sind ungekennzeichnete Verhaltensänderungen in der Programmiersprache natürlich ungewünscht, weil nicht orthogonal. Eine gewünschte Verhaltensänderung der Sprache stellen Kommentare dar. Der Zustand der Programmiersprache wurde geändert, so dass Anweisungen ab Beginn des Kommentars nicht mehr als solche gelesen werden.

Operatoren

Operatoren stellen besondere Funktionen dar, die nicht über einen Funktionsaufruf realisiert werden und ihre Parameter angehängt bekommen, sondern ihre Parameter in Form von Operanden erhalten. Operatoren treten als unärer Postfix-Operator (Operator hinter einem einzelnen Operanden: a++), als unärer Prefix-Operator (Operator vor einem einzelnen Operanden: &a) oder als binärer Operator (Operator zwischen zwei Operanden: a+b) auf. Zusätzlich existieren binäre Operatoren, wo die Operatoren Soweit entsprechen die Operatoren von Programmiersprachen den Operatoren in der Mathematik. Es existiert allerdings auch der „?:“-Operator, der drei Operanden benötigt. Dies kann als Serialisierung zweier binärer Operatoren interpretiert werden, oder als Umklammerung des zweiten Operanden durch „?“ und „:“, gefolgt von einem dritten Operanden.

C, C++, Java, C#:

wert = ( a == 1 ) ? Wert_fuer_wahr : Wert_fuer_falsch;

Operatoren verbinden Konstanten und Symbole entsprechend Ihrer Bedeutung. Basic-Dialekte nutzen gerne Operatoren in verschiedenen Bedeutungen. Die Programmiersprache PureBasic definiert a+1 mehrfach, einmal als Berechnung von a + 1, wenn das Ergebnis einer anderen Variable zugewiesen wird (b=a+1). Dabei wird a nicht verändert. Wird die Berechnung ohne eine Zuweisung durchgeführt, wird der „+“-Operator in der Anweisung a+1 als Inkrementierung von a verstanden (Langform a=a+1). Ähnliches gilt für den „=“-Operator, der zum einen als Zuweisung dient (b=a+1), in einem anderen Kontext eine andere Bedeutung erhält:

Basic:

if( b=a+1 )

Hier wird der ‚=’-Operator als Vergleich eingesetzt.

Datentypen

Datentypen beschreiben, wie Daten zur Verarbeitung gespeichert werden. Diese Beschreibung kann direkten Bezug auf die Maschine nehmen, so sind zum Beispiel viele praktisch orientierte Programmiersprachen mit Integer-Datentypen versehen, die den Registergrößen der Computer entsprechen. Bereits frühe Programmiersprachen erlaubten den Zusammenschluss vieler Daten eines Typs in einer Datenstruktur: das Array. Mit einem Struktur-Datentyp (struct, record, class) werden Datentypen strukturierbar, so dass ein Datentyp beliebig viele unterschiedliche andere Datentypen beinhalten darf. Diese Datentypen sind abstrakter als dass sie von der Maschine direkt zu verarbeiten wären, sie bestehen aus Meta-Daten über den Zusammenschluss beliebiger Datentypen, die vom Compiler anschließend maschinengerecht wieder aufgeschlüsselt werden, so dass der Zugriff auf die eingebetteten Datentypen auf die entsprechende Adresse umgesetzt wird. Neben Array und Struktur existieren weitere abstrakte Datentypen, die von der Maschine selbst (üblicherweise) nicht dargestellt werden können: Mengen, Listen und Strings. Auch Funktionen können als Datentyp auftauchen, z.B. in JavaScript.

Variablen

Im Gegensatz zu den Konstanten oder „Magic Numbers“ lassen sich Variablen im Programmverlauf verändern. Sie entsprechen einem beliebigen Speicherbereich, in dem sich Daten speichern lassen.

Sie erhalten in nahezu allen Programmiersprachen ein Symbol zugewiesen. Typorientierte Programmiersprachen verbinden eine Variable mit einem festen Datentyp (z.B. C, Java…), andere Sprachen verarbeiten alle Variablen vollständig ohne Datentyp (z.B. ARexx). Neben den festen Datentypen findet sich auch eine Reihe von Sprachen, die variable Datentypen zulassen, in Abhängigkeit zu den Daten, die zuletzt zugewiesen wurden (z.B. PHP, JavaScript).

Namensräume

Namensräume werden die Sichtbarkeitsbereiche von Symbolen/Namen genannt.

Das Problem wurde vom C++-Implementierer Bjarne Stroustrup aufgegriffen, um Bibliotheken unterscheiden zu können, die identische Funktions- oder Klassennamen verwenden. Doppelt auftretende Namen können damit einem Hersteller oder einer Bibliothek eindeutig zugeordnet werden. Durch die Deklaration der verwendeten Namensräume kann auf Funktionen direkt zugegriffen werden (z.B. in C++, C#). In Java werden die Namensräume explizit durchlaufen (zum Beispiel: system.out.println - Aufruf von println im Namensraum out; der Namensraum out befindet sich im Namensraum system).

Die Verwenden dieses Bausteins findet sich bereits bei frühen prozeduralen Sprachen, wo globale und lokalen Variablen unterschieden werden. Die ersten benannten Namensräume finden sich mit der Einführung von Klassen, wo eine Klasse automatisch ihren einen, gleichnamigen Namensraum definiert. Die Methoden-Definition in C++ erwartet vor dem Methoden-Namen zunächst den Klassen-Namen, also die Angabe, in welchem Namensraum die zu definierende Methode eingeordnet werden soll.

Namensräume selber tauchen in Programmiersprachen erst recht spät auf, sind in technischen Systemen jedoch häufig zu finden: Dateien liegen im Namensraum eines Verzeichnisses, d.h. die Name Programm.exe kann in unterschiedlichen Verzeichnissen mehrfach auftauchen, ohne die gleiche Datei zu meinen. Auch die WebSeite 'index.html„ kann auf allen Domains auftauchen, sie gilt nur für den durch die Domain vergebenen Namensraum. Weitere Beispiele als dem Alltag finden sich in Adressen: Staat.Land.Stadt.Straße.Hausnummer.Bewohner oder auch bei Telefonnummern:

Land / Vorwahl / Anschluss - Durchwahl

Ausdrücke

Ein Ausdruck stellt eine beliebige Kombination aus Operatoren, Symbolen und Konstanten dar. In diesem Sinne stellt ein einzelner Funktionsaufruf, eine einzelne Konstante oder ein einzelner Operator mit Operanden daher einen gültigen Ausdruck dar. Rechenoperatoren lassen sich so serialisieren, bzw. schachteln, beispielsweise über Operatorprioritäten oder Klammerung. Da Ausdrücke schachtelbar sind, können Operanden in Ausdrücken ebenfalls Ausdrücke darstellen.

r = (1 + a) * b( c ) + d[ 1 + e ];

Auch hier wird die Regel durch Implementierungsschwächen bestätigt, so lässt sich folgender Code in PHP nicht in der Version 4.1.2 ausführen, obwohl er für den Programmierer leicht nachzuvollziehen wäre:

PHP-Quelltext:

$variable = array( array( 1, 2, 3 ), array( 4, 5, 6 ) );
 
if( $variable[1][0] == 4 ) print "ok";
else                       print "fail";

Das für den Programmierer zu erwartende Ergebnis wäre der erste Eintrag im zweiten Array. Das Ergebnis der if-Abfrage wäre damit true. Es erscheint jedoch die Meldung „false“. Diese Anweisung ist in PHP (Version 4.1.2) nicht realisierbar und muss aufgesplittet werden:

PHP-Quelltext:

 $temp = $variable[1];
 if( $temp[0] == 4 ) print "ok";
 else                print "fail";

Hiermit erscheint die gewünschte Ausgabe „ok“. $variable[1] ist demzufolge für den PHP Interpreter nicht als Array greifbar, auf das anschließend mit [0] zugegriffen werden kann. Erst durch die Zuweisung des Ergebnisses auf die Variable $temp wird das eingebettete Array greifbar. Die Sprache befindet sich also nach dem ersten Arrayzugriff in einem Zustand, in dem kein Array-Zugriff mehr möglich ist und der vom Programmierer nicht erwartet wurde, die Sprache zeigte hier eine Schwäche im Hinblick auf die Orthogonalität.

Dieses Verhalten war offensichtlich ungewünscht und ist in der Version 4.3.3 entsprechend der Erwartungen des Programmierers geändert.

Speicheralloziierung

Es gibt fünf Möglichkeiten dem Programmierer Speicher zur Verfügung zu stellen. Dies sind gewissermaßen eigenständige Bausteine, aus denen sich das der Baustein 'Speicheralloziierung' zusammensetzt.

  • Direkter Zugriff: es wird keine Speicherverwaltung betrieben, es wird direkt an die passenden Adressen geschrieben (Assembler, C)
  • bei Stackorientierten Sprachen lassen sich Daten auf den Stack speichern (Asm, C, C#, Java)
  • es lässt sich Speicher vom Betriebsystem zuweisen (Asm, C, „Unsafe C#“, Embedded Java)
  • bei speicherverwaltenden Sprachen ergibt sich die Möglichkeit Speicher aus dem verwalteten Heap zuweisen (C#, Java)
  • untypisierte Variablen lassen sich beliebig verwenden, ohne dafür Speicher zu alloziieren (Basic-Dialekte, PHP, ARexx). Für dem Programmierer entspricht das dem direkten Zugriff, die Sprache selber handelt mit dem Betriebsystem jedoch auf eine beliebige Art die Speicherverwaltung aus. Es handelt sich um eine speicherverwaltende Sprache, die allerdings keine expliziten, typbeschreibenden Speicheranforderungen benötigt.

Speicheralloziierung ist in allen Sprachen erforderlich, um ein Programm eine Berechnung durchführen zu lassen. Selbstverständlich kann der Zustand eines kleineren Programms in den Registern des Prozessors gespeichert werden, so dass für ein aus einem Read-Only-Memory (ROM) laufenden Programm kein Arbeitsspeicher (RAM; Random-Access-Memory) benötigt wird. Mir ist jedoch keine Sprache bekannt, die sich ausschließlich auf den Speicher der Register konzentriert.

Garbage Collection

Auf welche Art auch immer Arbeitsspeicher alloziiert wurde, irgendwie muss er auch wieder an das System zurückgegeben werden. Die einzige Ausnahme bildet der direkte Zugriff, der allerdings bei den aktuellen Multitasking-Betriebsystemen nur noch vom Betriebsystem selbst durchgeführt werden könnte, bzw. über virtuelle Speicherbelegung emuliert werden müsste.
Die Rückgabe des Speichers geschieht beim Stack automatisch beim Verlassen der Funktion, bzw. des Programms. Beim alloziierten Speicher aus dem System-Pool muss dieser durch den Programmierer an das System explizit zurückgegeben werden, anderenfalls wird der Speicher bis zum nächsten Systemstart gar nicht mehr freigegeben, bzw. bei moderneren Betriebsystemen erst nach dem Ende des Programms.
Sprachen, die in diese Kategorien fallen, benötigen also keine von der Sprache gesteuerte Freigabe von Speicherresourcen.

Bei der Verwendung eines verwalteten Heap-Speichers ist die Sprache für die Freigabe von Speicherresourcen verantwortlich. Die Umsetzung wird in der Regel einfach implementiert: Wenn das Programm endet, wird der zuvor alloziierte Speicher freigegeben.
Java besitzt hierfür die Garbage Collection, die alloziierten Speicher wieder freigeben darf, sobald keine Referenz mehr auf die Klasseninstanz vorhanden ist. Dies wird allerdings recht kompliziert, wenn Objekte sich gegenseitig referenzieren. Tatsächlich läuft die Garbage Collection häufig erst an, nachdem große Mengen Arbeitsspeicher bereits verbraucht sind. Alternativ kann die Garbage Collection im Programm durch den Programmierer angestoßen werden.
Die Garbage Collection von JavaScript gibt gelegentlich auch Objekte frei, obwohl noch Referenzen existieren (gesehen in der Umsetzung des Internet Explorer). Die Folgen einer fehlerhaften Garbage Collection bedeuten, dass das Programm syntaktisch richtig ist, aber nicht ausgeführt werden kann. Der Programmierer muss zu Work-Arounds suchen ohne zu wissen, ob er überhaupt eine andere Möglichkeit finden kann. Die Garbage Collection trägt hier ein Risiko, eine Sprache bei einem Fehler vollkommen unbrauchbar zu machen.

Die Garbage Collection ist ein relativ neues Werkzeug, dass die Softwareentwicklung vereinfacht, in dem die Resourcenfreigabe durch die Sprache erledigt wird. Allerdings ist die Garbage Collection noch kein fertig entwickeltes Werkzeug, sondern wird laufend verbessert. Die derzeitigen Varianten haben noch bemerkenswerte Schwächen und so bleibt bei Java, wie auch bei C#, nur die Möglichkeit auf die Garbage Collection zu verzichten (Embedded Java oder „unsafe C#“).
Grade bei Embedded-Anwendungen, erweist sich die Garbage Collection eher als hinderlich, da diese Kleincomputer häufig Echtzeitanwendungen steuern und nicht über die nötigen Resourcen für eine Garbage Collection verfügen.

I/O

Jedes Programm muss die Möglichkeit besitzen, sich in irgendeiner Form dem Benutzer oder wenigstens dem Computer verständlich zu machen, muss Eingaben erhalten und Ergebnisse zurückliefern können. Die Ein- und Ausgaberoutinen stellen also grundlegende Funktionalität für jedes Computerprogramm bereit. Dazu müssen Möglichkeiten existieren, um von Eingabegeräten wie Tastatur oder Festplatte Daten lesen zu können und auf Ausgabegeräten wie Bildschirm, Drucker oder Festplatte schreiben zu können.
Hier gibt es unterschiedliche Ansatzpunkte. Basicdialekte oder Skriptsprachen beinhalten in der Regel die notwendige Funktionalität in der Sprache, zum Beispiel über input und print-Befehle. Sprachen, wie C, C++, Java oder C# gliedern diese Funktionalität in Bibliotheken oder Komponenten aus. Das bedeutet, dass sie über keine eigene integrierte Funktionalität verfügen, um Eingabe- oder Ausgabeaktivitäten zu steuern, sondern externe, fertige Routinen einbinden und aufrufen. Die zweite Methode orientiert sich an der Idee, dass Programme nicht grundsätzlich auf Festplatten oder Konsolen schreiben, sondern auch über alternative Ein- und Ausgabemöglichkeiten, wie zum Beispiel die grafische Benutzeroberfläche verfügen können. Dabei werden Konsolen-Ausgaben folglich nicht benötigt.

Bibliotheken und Komponenten

Ein Unterprogramm oder eine Sammlung von Unterprogrammen zu einem Softwareproblem wird als Komponente bezeichnet. Eine Komponente stellt nur einen (in der Regel spezialisierten) Teil einer Software dar und erweitert die eigentliche Software so, dass sie lauffähig wird oder zusätzliche Funktionalität aufweist. Dies entspricht soweit dem Klassenkonzept, wo spezialisierte Klassen einen Bereich des Programms steuern. Bei einer Bibliothek wird diese Funktionalität in eine gesonderte Datei ausgelagert, die die kompilierten Funktionen enthält. Im Falle einer System-Bibliothek wird die Komponente vor dem Programmstart geladen und durch das Betriebsystem dynamischen verlinkt. Bei Bedarf kann das Programm über das Betriebsystem weitere Libraries öffnen (z.B. Dynamic Link Library, DLL-Datei bei Windows).
In beiden Fällen werden kompilierte, native vorliegende Programmteile mit dem zu startenden Programm verbunden. Im ersten Fall handelt es sich um Bibliotheken, die für den Programmstart essentiell wichtig sind, ohne die das Programm nicht starten kann. Hier wird das dynamische Linken durch den Lader des Betriebsystems vorgenommen.
Im zweiten Fall wurde das Programm bereits gestartet und fordert nun weitere Funktionalität an. Bibliotheken können so, ebenso wie Klassen, von mehreren Programmen verwendet werden. Da die Bibliothek bereits kompiliert wurde, müssen die Quelltexte nicht mehr vorliegen, um die Funktionalität zu nutzen.

Damit ein kompiliertes Programm grundlegende Funktionalität nicht in jedes Programm statisch einbinden muss, kann regelmäßig verwendete Funktionalität in Bibliotheken ausgelagert werden. Dazu gehören Ein-/Ausgaberoutinen, spezielle Berechnungen, 3D-Grafik, Treiber für das Betriebsystem und viele weitere Funktionen.

Eine Bibliothek wird auch als Modul bezeichnet. Programme, die auf Modulen aufbauen und per Plug-In-Module erweiterbar sind, werden als modular bzw. modular programmiert bezeichnet.

Die Unterstützung von Plug-Ins, Modulen oder Komponenten wird in der Regel durch das Betriebsystem geleistet, zum Beispiel durch DLL-Dateien.

Templates/Generics

Nachdem die objektorientierten Programmierung sich inzwischen allgemein durchgesetzt hat, gewinnt die generische Programmierung an Bedeutung. Dabei werden Algorithmen unabhängig vom Datentyp beschrieben, so dass zum Beispiel Berechnungen für Integer, Fließkommazahlen oder auch selbst geschriebene Klassen, die über entsprechende Funktionen bzw. Operatoren verfügen, über einen einzigen Algorithmus realisiert werden.
Der Algorithmus muss so nicht für verschiedene Datentypen mehrfach geschrieben werden, der Quellcode wird so für beliebige Datentypen wiederverwertet.

In C++ findet sich die Umsetzung so, dass ein Template für jeden benutzten Datentyp speziell übersetzt wird. Realisiert man eine Integer-Liste und eine Liste für einen anderen Datentyp über ein Listen Template, so finden sich im kompilierten Programm für beide Datentypen eine eigener Part, der die zugehörige Liste beschreibt. Der Code taucht also im ausführbaren Programm mit kleinen Modifikationen zweimal auf.

Regeln

Regeln sind eine besondere Form von Funktionen. Sie werden bei Zustandsänderungen des Programms aktiv und nicht vom Programmierer explizit gerufen. Dieser Baustein findet sich in Objective Pascal (Delphi) und C# als Property realisiert und wird in Java und C++ als „Getter“ und „Setter“-Funktionen abgebildet. Alle Sprachen unterstützen Properties allerdings nur mit einem einzigen „Argument“, ein Property kann nur einem einzigen Symbol zugeordnet werden: dem Propertynamen.
So wird das Setter-Property ausgeführt, wenn ein versucht wird, die Property-Variable zu verändern. Die Setter-Property kann nun eine Regelung festlegen, ob eine Variable verändert wird und welche.

Grundsätzlich könnte eine Regel allerdings auch mehrere Variablen gleichzeitig betreffen, oder auch den Zugriff auf ein Array kontrollieren. Regeln werden weitergehend im Kapitel ‚Vermeidung von Redundanzen’ behandelt.

Dieser Baustein 'Regeln' entspricht nicht den Regeln, wie sie in Prolog zu finden sind.
Eine Prolog-Regel arbeitet als Getter-Funktion, wenn eine oder mehrere (uninitialisierte) Variablen übergeben werden. Das Resultat ist dabei vergleichbar mit einer SQL-Abfrage an eine lokale Datenbank.
Werden stattdessen Atome (bzw. initialisierte Variablen) übergeben, so arbeitet die Prolog-Regel als Funktion, die die übergebenen Parameter gegen die Faktenbasis abgleicht und einen Wert „Yes“ oder „No“ zurückliefert. Auch dies ließe sich mit einer SQL-Abfrage klären - hier wird jedoch lediglich geprüft, ob die Faktenbasis eine passende Zeile mit allen gesuchten Daten „joinen“(zusammenfügen) kann.
Prolog-Regeln stellen daher keinen eigenständigen Baustein dar, sondern ergeben sich durch die Kombination mehrerer Bausteine in einem Sprachkonstrukt und entsprechend geschickt gewählter Syntax und Semantik.

Ausnahmebehandlung

Die Ausnahme (Exception) beschreibt einen außerordentlichen Laufzeitfehler, der im eigentlichen Algorithmus nicht geprüft werden soll. So bleibt der eigentliche Algorithmus frei von Fehlerabfragen übersichtlicher. Falls das Programm eine Ausnahme erzeugt, können die möglichen Fehler aufgefangen werden. Nicht abgefangene Fehler werden zur aufrufenden Funktion durchgereicht.
Exceptions haben Sinn, wenn man Fehlerbehandlung und Algorithmus voneinander trennen möchte und besonders, wenn die Fehlerbehandlung nicht sofort von der aufrufenden Funktion erledigt werden soll. In diesem Fall muss ein Fehlercode von Hand Funktionsaufruf für Funktionsaufruf übermittelt werden, im Falle einer Exception wird dieser Part automatisiert.
Neben diesen Vorteilen haben Exceptions auch eine Schwäche im Konzept: Fehler, die an vielen Stellen auftreten können, können auch an vielen Stellen eine Ausnahme auslösen, als Beispiel sei die Ausnahme beim Zugriff auf einen nicht existierenden Index eines Arrays genannt.
Um nun sicher den Fehler passend zur Position abzufangen und darauf angemessen zu reagieren, müssen Ausnahmebehandlungen geschachtelt werden. Dies wirkt sich auf die Lesbarkeit des Sourcecodes wieder kontraproduktiv aus.

Präprozessoren

Ein Präprozessor stellt eine einfache Sprache zur Zusammenbau von Texten, z.B. Quelltexten, dar. Der Quelltext kann so vor dem Kompilieren in eine andere Form gebracht werden, es können Teile des Quelltextes abhängig von Bedingungen eingefügt oder ausgelassen werden. Der Präprozessor kann so benutzt werden, um Makros einzufügen oder Magic-Numbers mit Text zu umschreiben. Der Name des Makros bzw. der Magic-Number wird vor dem Kompilieren durch den passenden Text ersetzt.
Ein gutes Beispiel für einen Präprozessor ist die Sprache PHP (rekursives Akronym für „PHP: Hypertext Preprocessor“), die umfrangreiche Funktionalität bietet, um die Beschreibungssprache HTML zusammenzusetzen. HTML wird anschließend natürlich nicht kompiliert, sondern direkt an den Web-Browser des Clients verschickt und anschließend dort interpretiert.

Attribute

Attribute verfeinern die Beschreibung, die Datentypen implizit mitgegeben werden. Sie nehmen damit Einfluss auf die Semantik eines Datentyps (zum Beispiel „long“ oder „unsigned“). Attribute dienen ebenso der Kontrolle des Quelltextes und klären so die Sichtbarkeit von Symbolen in vielen Sprachen („public“, „protected“, „internal“, „private“).

Einheiten

Die Verarbeitung von Einheiten stellt einen Baustein dar, den ich erst vor wenigen Jahren in der Beschreibungssprache 'Cascading Style Sheets' (CSS) erstmals bemerkte. Aber auch LaTeX erlaubt die Angabe und Verrechnung von Einheiten. Hierbei können Zahlen einen Datentyp erhalten, der eine Zahl von einer anderen Zahl unterscheidet. Als Beispiel:

 unit cm long Breite;
 unit a4 long AnzahlBlaetter;
 
 Breite = 12cm;
 AnzahlBlaetter = 35 a4; // (in Anlehnung an die Aufgabe x=35a4)

Einheiten erlauben die Klassifizierung von Zahlenwerten. Der Entwickler wird aufgefordert, eine Zahl mit ihrer Bedeutung bzw. einem Maßstab zu versehen, so dass ein geometrisches Objekt Methoden zur Verfügung stellen kann, die Parameter in Zentimetern oder in Metern akzeptieren, aber Parameter in beispielsweise Kilowatt ohne Casting durch den Entwickler ablehnen.
Einheiten sind damit eine besondere Form eines Attributes.

Bausteine verbinden

Der erste Baustein, der mich auf diese Unterteilung von Programmiersprachen-Elementen brachte, war die sprachliche Unterscheidung von Funktionen (mit Rückgabe) und Prozeduren (ohne Rückgabe) in Pascal. C verliert diese Unterscheidung nicht, aber in der Grammatik der Sprache beschreibt eine Prozedur nicht als Sonderfall einer Funktion. Eine Prozedur ist eine Funktion mit dem Rückgabedatentyp „nichts“ (void). C fasst diese beiden Bausteine zusammen in einen Baustein, der Unterprogramme unabhängig vom Rückgabewert definieren kann. Dies bedeutet, dass dem Syntaxparser der Unterschied zwischen „procedure“ und „function“ nicht beigebracht werden muss, sondern sich der Rückgabewert „void“ in die übrigen Datentypen eingliedert. Das Sprach-Konzept kennt damit die Unterscheidung zwischen Prozedur oder Funktion überhaupt nicht, die Verwendung ist identisch, es existiert grammatikalisch in jedem Fall ein Rückgabetyp, auch wenn dieser „Nichts“ ausdrückt.

Diese Logik besitzt neben dem Vorteil für den Implementierer zwei weitere Vorteile. Der Programmierer hat (geringfügig) weniger Aufwand, wenn eine Prozedur nun doch einen Rückgabewert liefern soll und der Lernende muss weniger Konzepte lernen, weil er durch die Kombination bereits bekannter Schlüsselwörter Prozeduren und Funktionen nachbilden kann. Die Sprache wird kleiner, ohne an Ausdrucksmöglichkeiten zu verlieren. Der Sonderfall „Prozedur“ wurde durch eine weniger spezialisierte Definition von „Funktion“ überflüssig.

Die wichtigste Erkenntnis, die sich daraus ergibt, ist dass eine Sprache mit weniger, aber allgemeineren Bausteinen flexibler wird und mehr Kombinationsmöglichkeiten bietet. Es ist also zu überlegen, ob sich andere Standardbausteine ebenfalls durch eine geringere Zahl allgemeiner definierter Bausteine ohne Verluste ersetzen lassen.

Nicht alle Bausteine wurden in allen Programmiersprachen verwendet, bzw. vorhandene Bausteine sind unterschiedlich in den einzelnen Sprachen implementiert. C unterstützt abstrakte Datentypen, wie Strukturen, aber keine Listen. Listen und ihre Funktionalität (suche erstes Element, suche nächstes Element, gebe Daten zurück…) müssen in C zunächst beschrieben werden. Andere bekannte Konstrukte ergeben sich aus der Kombination von Bausteinen: Datentypen können ohne Funktionen (zum Beispiel in alten Basic-Dialekten) verwendet werden und Funktionen können ohne Datentypen verwendet werden (zum Beispiel ARexx). Durch die Kombination von Unterprogrammen und Datentypen entsteht das Klassenkonzept. Sind die Datentypen in der Lage Methodenzeiger zu verwalten, ist objektorientierte Programmierung möglich.

Sicherlich sind objektorientierte Sprachen oder Prolog nicht durch die Betrachtung von Bausteinen entstanden, allerdings lässt sich so ein alternativer Zugang zu diesen Sprachkonzepten finden. Es stellt sich die Frage, ob sich Einzelkonzepte verbinden lassen, um so weniger spezialisierte und flexiblere Konzepte zu gewinnen. Durch die Verallgemeinerung gehen keine Möglichkeiten verloren, es ist zu prüfen, ob sich dadurch neue Konzepte ergeben oder sich Programmierparadigma einvernehmlich, orthogonal und praxisorientiert in einer einzigen Programmiersprache verbinden lassen.

Operatoren und Konstrukte

Wer eine Programmiersprache entwickeln möchte, besitzt in der Regel Assemblerkenntnisse, mindestens jedoch Programmierkenntnisse in anderen Sprachen. Schaut man nochmals zurück auf Assembler, wo alle Anweisungen Konstrukte darstellen, so fällt auf, dass viele Programmiersprachen bei Berechnungen Ausdrücke erlauben, bei Entscheidungen jedoch weiterhin auf Konstrukte setzen.
Eine Entscheidung wird getroffen, zum Beispiel über if, es wird der jeweilige Zweig ausgeführt und anschließend wird das Programm nach dem Konstrukt fortgesetzt.
Ein Konstrukt liefert keine Rückgabe, das bedeutet, dass die Information, ob nun der Then-Zweig oder der Else-Zweig ausgeführt wurde, verloren ist. Aus diesem Grund muss für solche Konstrukte eine zusätzliche Variable eingeführt werden, die je nach Zweig unterschiedlich gesetzt wird. “<condition>„ steht in diesem Zusammenhang für eine beliebige Bedingung.

bool result;
 
if( <condition> )
{
   result = true;
   do_anything();
}
else
   result = false;
 
return result;

alternativ:

bool result = <condition>;
 
if( result )
  do_anything();
 
return result; 

Möchte man auf die Definition einer zusätzlichen Variablen verzichten, so muss die nachfolgende Verwendung auf den jeweiligen Zweig gezielt ausgerichtet werden. Das ist in diesem Fall lediglich der Rückgabewert der Funktion und von daher nur dann kritisch, wenn man großen Wert auf ein einziges Return innerhalb einer Funktion wert legt:

if( <condition> )
{
   do_anything();
   return true;
}
else return false;

Trotzdem bleibt das Return redundant, so ergibt sich vielfach folgende Alternative:

if( <condition> )
  do_anything()
 
return (<condition>);

Hierbei ist wieder sichergestellt, dass die Funktion nur einen Ausgang hat und sie benötigt auch keine zusätzliche Variable. Leider wird die Bedingung wiederholt ausgeführt - sollten für die Bestimmung des Bedingungs-Wertes Funktionsaufrufe oder andere wertverändernde Operationen notwendig sein, so könnte die wiederholte Verwendung einer Funktion eventuelle Nebeneffekte verursachen - der Hauptkritikpunkt an Makros mit Hilfe des C-Präprozessors.

Dabei wurde die Bedingung bereits vollständig ausgewertet und das Ergebnis war bekannt, sonst hätte ja nicht verzweigt werden können. Würde diese Information abfragbar, also zurückgegeben, so würde if als Operator auftreten und die Anweisung könnte auf einen Ausdruck verkürzt werden:

return if( <condition> )
    do_anytyhing();

Dies stellt eine zusätzliche Möglichkeit dar, um diesem Sachverhalt zu begegnen. Sie ist orthogonal, strukturiert, logisch nachvollziehbar, weil der Wert zurückgegeben wird, der sich durch den Ausdruck „if( <condition> ) …“ ergeben hat. Da die Schreibweise kurz ist, ist sie auch schneller zu erfassen, als die zuvor aufgeführten Alternativen.
Auch else wird damit zum Operator, der Bezug auf die Rückgabe von if nehmen kann. Wenn der Ausdruck links von else den Wert false besitzt, dann führe folgende die nachfolgenden Anweisungen durch. Der Rückgabewert entspricht unverändert dem linken Parameter. Else löst sich damit als Teil des if-Konstruktes und kann als eigenständiger Operator verwendet werden, der - wie if - Programmcode in Ausdrücke einbringen kann.

Diese Vorgehensweise lässt sich durch alle anderen Entscheidungen treffenden Konstrukte fortsetzen:

while( a-- < 10 )
  if( a != 5 ) do_anything();
  else break;
 
if( a >= 10 ) 
  printf("Schleife vollständig durchlaufen und nicht abgebrochen");

kann geschachtelt werden:

if( !while( a-- < 10 )
     {
       if( a != 5 ) do_anything();
       else break;
     }
  ) printf("Schleife vollständig durchlaufen und nicht abgebrochen");

Die Abfrage wird wahr, wenn while falsch zurückliefert: ein Fall für den else-Operator:

while( a-- < 10 )
  if( a != 5 ) do_anything();
  else break;
else 
  printf("Schleife vollständig durchlaufen und nicht abgebrochen");

Diese Idee des while-else wirkt im ersten Moment ungewohnt, ist allerdings nicht mehr neu: das while-else-Konstruktes wurde bereits in der Programmiersprache Python realisiert.

In „Design und Entwicklung von C++“[1] spricht sich Bjarne Stroustrup, Erfinder von der Programmiersprache C++, für ausdruckorientierte Sprachen aus. So tauchen bereits in C Operatoren als Schlüsselwörter auf: sizeof(), in C++ erscheinen mit new und delete weitere Schlüsselwörter, die nicht als Konstrukt umgesetzt sind. Die Vorgabe, dass die verzweigenden Konstrukte kein Resultat ihrer Handlung zurückliefern, wirkt hier willkürlich gesetzt und eher ungeprüft aus historischen Sprachen übernommen.

Funktionen, Operatoren, Properties und Variblenzugriffe Funktionen im ursprünglichen Sinne (haben im Gegensatz zu Prozeduren einen Rückgabewert), Operatoren und Variablen geben Rückgabewerte zurück und besitzen einen Datentyp. Getter-Properties als parameterlose Funktionen können in C# wie eine Variable verwendet werden, besitzen also ebenso einen Rückgabewert mit zugehörigen Datentyp.

result = v;    // Variable
result = p;    // Getter-Property
result = f();  // Funktion

Zwischen Funktionen und Getter-Property bestehen technisch keine Unterschiede: ein Getter-Property ist eine Funktion ohne Parameter. Eine Variable kann durchaus als Funktion angesehen werden, die lediglich die Adresse der Datenposition berechnet und als Return-Wert den Inhalt der Speicheradresse liefert. Der Zugriff auf eine Variable stellt intern also durchaus eine Funktion dar, die jedoch nicht gerufen wird, sondern grundsätzlich „inline“ in die rufende Funktion einkompiliert wird.
Selbiges gilt für Operatoren: Operatoren, die als Assemblerbefehl umsetzbar sind, können durchaus als „inline“-Funktion verstanden werden, die diesen Assemblerbefehl emittieren. Das Überladen von Operatoren mit eigenen Funktionen entspricht einem normalen Funktionsaufruf, inklusive Sprung und für den Funktionsaufruf benötigter Stackverwaltung.

Selbiges gilt für den umgekehrten Fall: das Beschreiben von Symbolen. Funktionen sind in der Regel als Nur-Lesend markiert, in einer kompilierenden Sprache ist es also nicht möglich, eine Funktion zur Laufzeit zu überschreiben. Überschreiben bedeutet, dass die bekannte Funktion gelöscht und durch eine alternative Funktion ersetzt wird; entsprechend ist Überschreiben hier nicht mit Überladen zu verwechseln, wo mehrere Funktionen unterschiedlicher Signatur über das gleiche Symbol ansprechbar sind. Operatoren lassen sich in allen mir bekannten Sprachen nicht überschreiben.

Auf Variablen und Properties lassen sich jedoch Zuweisungen tätigen:

v = 1;   // Variable
p = 2;   // Property
f() = 3; // Fehler

Die Variable und das Setter-Property lassen sich hier wieder einer Funktion gleichstellen. Wichtig ist die bei den Properties deutlich ausgearbeitete Aufteilung, die die Zuweisung und das Auslesen einer Variablen als zwei vollkommen unterschiedliche Vorgänge wiedergibt. Sobald eine Property verändert wird, wird sie über eine Setter-Funktion überschrieben.
Dies gilt auch für Variablen: hier wird das Überschreiben vom Compiler implizit implementiert, in dem der neu berechnete Wert aus dem Prozessorregister (oder einer Speicherstelle) in die Speicheradresse der Variablen kopiert wird.

Ebenso implizit werden Operatoren und Funktionen als Read-Only deklariert, beziehungsweise, der Programmierer kann ausschließlich den Getter-Part des Operators überladen.
Die Zusammenlegung dieser Bausteine in einen großen, allgemeineren Baustein würde bedeuten, dass aus dem Baustein heraus die Möglichkeit entsteht, Setter-Routinen auch für Operatoren und Funktionen zu schreiben.

Lassen sich Funktionen, Operatoren, Properties und Variablen in einen einzigen Baustein vereinen, bedeutet das in jedem Fall die aufwendigste Implementierung. Diese wäre für einen Einzelfall (den Setter bei den Properties) ebenfalls erforderlich, wird die aufwendige Variante aber grundsätzlich genutzt, ergeben sich wieder weniger Sonderfälle. Der Setter muss nicht unterschieden werden und auch nicht als besonderes Merkmal gelernt werden.
Damit wäre für die Implementierung bereits ein Vorteil gewonnen. Ob die Möglichkeit Setter-Routinen für Funktionen und Operatoren schreiben zu dürfen wirklich sinnvoll ist, erscheint im Augenblick fraglich. Von einem distanzierten Standpunkt erhöht es die Kombinationsfähigkeit der Sprache und nimmt für den Programmierer eine Sonderregel aus der Sprache. Die Sprache bleibt uneingeschränkt orthogonal und offen für ungewöhnliche Anwendungen.

Als Kritik bleibt natürlich zunächst, dass Operatoren für unsinnige Anweisungen genutzt werden könnten.

Die Anweisung:

1 + 2 = 18 + 2;

könnte damit einen gültigen Programmcode darstellen, der einem Aufruf der des Operators “+„ mit den Parametern 1, 2 und dem Set-Wert 20 entspricht. Hierbei ist klar herauszustellen, dass Operatoren in einer Programmiersprache mathematischen Operatoren eben nicht entsprechen und es vermutlich keinen Sinn macht eine implizierte Setter-Funktion (die hier sinnvolle Fehlermeldung) bei den mathematisch inspirierten Operatoren zu überschreiben. Das Beschreiben von Operatoren grundsätzlich zu verbieten, kann allerdings auch nicht der richtige Weg sein.
Die durchgehende Orthogonalität der Sprache und das Erlauben von experimentellen Programmierstilen wiegen hier in meinen Augen stärker, als die Vorgabe von Regeln. Regeln, wie das Vermeiden ungeschickten Neudefinitionen von Operatoren, die zu Ausrücken wie „1+2=18+2“ führe, kann sich jeder für sich aufstellen, der dem Grundsatz entspricht eines intelligenten Programmierers entspricht.

An späterer Stelle wird sich zeigen, dass das Beschreiben von Operatoren durchaus sinnvolle Funktionalität mitbringen kann.

Ein Operator ist - wie eine Funktion - die Beschreibung einer Handlung. Werden Operatoren gleichbedeutend mit Funktionen verstanden werden, bedeutet dies, dass Funktionen ihre Entsprechung in Prefix-Operatoren finden. Ein Operator wird damit ansprechbar über ein beliebiges Symbol: den Funktionsnamen. Im Umkehrschluss bedeutet dies, dass Postfix- und binäre Operatoren nicht ausschließlich über nichtalphabetische Symbole angesprochen werden müssen, sondern durchaus auch Buchstaben enthalten können. Mit new und delete existieren ja bereits derartige Operatoren.
Es muss also möglich sein, Funktionen nicht nur als Präfix-Operator zu definieren, sondern ebenfalls Symbole als binäre bzw. Postfix-Operatoren zu definieren. Diese Art zu Programmieren findet sich in der objektorientierten Programmierung über den „.“-Operator wieder. Dabei erhält die Funktion das Präfix-Argument als Variable „this“ übergeben:

MyVar.MyFunc( MyValue );

Entsprechend lassen sich bereits gut verständlichere Operationen über Funktionen abbilden:

C, Java, C#-Sourcecode
Auto.oeffne( Tuer );
Wasser.fuelleIn( Topf );

Als Operator formuliert können die Verbindungsoperatoren zwischen Klasse und Funktion, bzw. zwischen Funktion und Parameter wegfallen:

Auto oeffne Tuer;
fuelle Wasser in Topf;
in Topf fuelle Wasser;

Durch entsprechende Ausformulierung von Operatoren nähert sich die Programmiersprache damit einen weiteren Schritt der menschlichen Sprache ohne die Eindeutigkeit der Anweisungen zu verlieren.

Zwischen den Symbol-Operatoren und den Operatoren, die ausschließlich aus Seperatoren aufgebaut sind, existiert ein gravierender Unterschied: Operatoren aus Seperatoren beginnen und enden zwangsläufig mit Seperatoren. Am Anfang und Ende befinden sich also Zeichen, die sich eindeutig von Symbolen unterscheiden. Dies muss auch bei Symbol-Operatoren der Fall sein, da sie ohne Seperatoren nicht von anderen Symbolen zu unterscheiden sind:

fehlerhafter Code: AutooeffneTuer; fuelleWasserinTopf;

Als Trennzeichen bieten sich Seperatoren an, die sonst keine weitere Funktion übernehmen: das Leerzeichen, CarrigeReturn, LineFeed oder Tabulator. Ein Symbol-Operator, erwartet an Start und Ende einen Seperator. Sofern kein Seperator vorgegeben ist, wird ein funktionsloser Seperator erwartet, anderenfalls wird kann gefundene Symbol nicht als Operator erkannt werden.

Um die Eindeutigkeit zu garantieren, bedarf es drei weiterer Regeln:

Ein Operator sollte nicht gleichzeitig als Prefix-, Postfix und binärer Operator benutzt werden, um Verwechslungen zu vermeiden

Beispiel:

&Flags;
Flags&;
Flags & Wert;

Hier ist im zweiten Fall nicht entscheidbar, ob nach dem Postfix-Operator noch ein Wert folgen muss oder nicht.

Ein Operator sollte nicht als Postfix und als binärer Operator definiert sein

Dies erschließt sich aus dem einfachen Grund, da nicht sicher gesagt sein kann, wann ob nach dem Operator nun noch ein Argument folgen soll, weil es sich um einen binären Operator handelt oder ob ein Operator erwartet wird, da es sich um den Postfix-Operator handelt.

Diese Regeln stellt jedoch kein explizites Verbot dar, eher einen Hinweis, um keine unschönen Klammerungen und eventuelle Fehler durch vergessene Klammern zu riskieren.

Bestandteile unärer Operatoren dürfen nicht Bestandteilen binärer Operatoren verwechselbar sein

Dieses Problem betrifft lediglich die üblichen, aus Seperatoren bestehenden, Operatoren. Ein Prefix-Operator kann mit einem binären Operator kollidieren, wenn der binäre Operator mit einen oder mehreren Seperatoren dem Ende des vorhergehenden Seperators entspricht. Dabei kann ein Operator den anderen „verschlucken“ oder es entsteht die Möglichkeit zwei Operatoren mehrdeutig zu interpretieren.

Die Möglichkeit des „Verschluckens“ existiert bereits:

  long result, a = 8, *pa = &a;  // pa zeigt auf a
 
  result = 16/*pa;    // 16 geteilt durch den Wert, auf den pa zeigt.

Dieser Sourcecode ist syntaktisch richtig, lässt sich aber mit dem GCC-Compiler nicht kompilieren. Es wird sich vermutlich auch kein Compiler finden, der in der Lage ist, dieses Programm zu kompilieren, da der Compiler nicht versteht, was hier beschrieben ist. Eine Prüfung auf Operator-Kollisionen findet nicht statt, der GCC meldet am Ende der Datei den Fehler 'nicht beendeter Kommentar'. Hier treffen der Operator Division ('/') und Dereferenziere ('*' als Prefix) direkt aufeinander, der Compiler liest daraus Kommentarbeginn ('/*'); es folgt ein unbeabsichtigter Kontextwechsel: es werden Kommentare gelesen.

Auszugehen ist zwangsläufig vom jeweils (zeichenmäßig) größten Operator. Treten solche Fälle auf, kann der Programmierer keine Warnung erhalten, dass er den Operator deutlicher zuordnen sollte, da der Compiler sonst bei jedem Kommentarbeginn eine Warnung ausgeben müsste. Das Problem lässt sich durch zusätzliche, funktionslose Seperatoren klären, die die Operatoren trennen. Im Falle des oben stehenden Beispiels können die Operatoren durch ein Leerzeichen getrennt werden:

result = 16 / *pa; // 16 geteilt durch den Wert, auf den pa zeigt. 

Datentypen und Namensräume

Schaut man sich die gängigen Programmiersprachen zur Anwendungsprogrammierung an, so hat sich das Konzept der Namensräume in den Sprachen der letzten Jahre durchgesetzt. Neue Sprachen wie Java und C# importieren Namensräume, um Komponenten einzubinden, im Gegensatz zu C++, das Quelltext-Header-Dateien einbindet, in denen die Namenräume definiert werden müssen. Die Integration der Namensräume in die Sprachen schreitet also voran, die Compiler erhalten also viel früher einen Überblick über die dem Programmierer verfügbaren Namensräume. Um die Komponenten einbinden zu können, müssen sie einen direkten Zugriff auf alle installierten Komponenten haben. Damit rücken Compiler und kompilierte Komponenten zusammen, wie es in C++ nicht möglich ist.

Neue Namensräume werden (in C++, C#) mit dem Schlüsselwort „namespace“ eingeleitet, innerhalb des neuen Namensraumes kann anschließend beliebig programmiert werden. Es können Variablen angelegt werden, die ausschließlich innerhalb dieses Namensraumes sichtbar sind, es können Funktionen definiert werden, es können abstrakte Datentypen und untergeordnete Namensräume definiert werden.

Abstrakte Datentypen wie Strukturen oder Klassen haben die Eigenschaft, dass sie einen eigenen Namensraum erzeugen. Eine Variable a unterscheidet sich von einer Variablen a, die in einer Klasse MyClass eingelagert ist. Die Adressierung findet in der Regel über eine Variable des entsprechenden Typs statt:

MyClass var;
 
var.a = 1;

Ähnliches gilt für C++, wo Klassendeklarationen (in den Header-Dateien) und Methoden-Definitionen (in den CPlusPlus-Dateien) getrennt werden. Hier geschieht die Definition der Methoden in den Namensraum der Klasse hinein:

C++-Header:

class MyClass
{
  // Deklaration
  void MyFunction( void );
}

C++-Code-Datei:

void MyClass::MyFunction(void)
{
}

Vor den “::„-Operator wird in C++ der Namensraum der zu definierenden Funktion spezifiziert.

Der Aufruf von statischen Member (also Variablen oder Funktionen, die einmalig für alle Instanzen einer Klasse sind) wird in C# beispielsweise durch die Verknüpfung des Klassennamens und des Funktionsnamens durchgeführt.

C#-Sorucecode:

MyClass MyVar = new MyClass();
 
MyClass.MyStaticFunction();
MyVar.MyRegularFunction();

Eine Klasse öffnet einen eigenen Namensraum. Innerhalb einer Klasse können anschließend wieder Variablen, Funktionen und abstrakte Datentypen definiert werden, genauso, wie in einem Namensraum.
Es gibt also nur marginale Unterschiede zum Konzept eines Namensraumes: Ein Namensraum ist eine Klasse, die nicht instantiiert werden kann und besitzt entsprechend auch keine Konstruktoren und keinen Destruktor. Variablen können in einem Namensraum vorinitialisiert werden, dies entspricht einem statischen Konstruktor, der statische Variablen innerhalb einer Klasse initialisiert.

Namensräume sind in C++ und C# keine Datentypen. Namensräume entsprechen jedoch Klassen, die ausschließlich über statische Member verfügen. Eine Instanz einer Klasse, die ausschließlich über statische Member verfügt, besitzt keine variablen Member und damit keinen instantiierbaren Inhalt: Die Instanz wäre 0 Bytes groß, da alle statischen Member für alle Klassen genau einmal existieren, die Instanz selber hätte keine individuellen Daten.

Ein Namensraum kann aber über eine Klasse mit ausschließlich statischen Membern realisiert werden und stellt damit nur einen Sonderfall einer Klasse dar, der über ein eigenes Schlüsselwort angesprochen werden kann.

Speicherallozierung, Garbage Collection und Datentypen Eines der schwierigsten Themen bei der Überlegung, wie eine neue, praxisorientierte Sprache aussehen könnte, war die Überlegung, ob eine Garbage Collection wirklich Sinn hat. Die Garbage Collection ist aufwendig zu implementieren. So aufwendig, dass selbst eine große Firma wie Sun die Garbage Collection nur über den Trick, die Garbage Collection manuell zu starten, sinnvoll am Laufen hält. Eine Garbage Collection disqualifiziert eine Sprache für Realtime-Anwendungen, für Embedded Systems und jede Software, die effizient mit dem vorhandenen Arbeitsspeicher arbeiten soll. Dies sind schwere Argumente gegen die Verwendung einer Garbage Collection ins Rennen wirft, die Verwendung einer Garbage Collection also eigentlich verbieten würde.

Fakt ist aber auch, dass sie von den Entwicklern akzeptiert wird, da sie dem Entwickler die Verantwortung für ein altbekanntes Problem abnimmt: die Vermeidung von Speicherleichen. Eine Garbage Collection wird im späteren Verlauf der Überlegungen sowieso Pflicht für eine Programmiersprache, wenn abstrakte Datentypen wie Strings realisiert werden sollen. Vielleicht macht eine allgemeine Garbage Collection trotz des Aufwandes durchaus Sinn.

Die allgemeine Akzeptanz von Java stieg erst mit der Verwendung von stärkeren Prozessoren und vor allem mehr Arbeitsspeicher. Die Akzeptanz ergibt sich, wie erwartet, dadurch, dass man Verantwortung an die Sprache abgeben kann. Das Programm läuft im Rahmen der technischen Möglichkeiten von Java, benötigt viel Arbeitsspeicher, hinterlässt allerdings kein Speicherleck, wenn es beendet wurde. Der Programmierer macht an dieser Stelle keinen Fehler mehr, wenn er „vergisst“ Speicher freizugeben, für Resourcenverschwendung ist nicht der Entwickler selber schuld, sondern die Implementation der Sprache. Dies ist jetzt Stand der Technik. Die Garbage-Collection erweist sich je nach Programm als so resourcenintensiv, dass es speichersparender wäre, das Programm in C++ zu schreiben und den Speicher einfach nicht freizugeben. Nach Beendigung des Programms, garantiert ein modernes Betriebsystems, wie Linux oder WindowsXP, die Speicherfreigabe durch die Prozessverwaltung ebenfalls.

In meiner Umfrage stellte ich die Frage, für wie wichtig die Garbage Collection befunden wurde. Da diese Frage vorrangig Personen betrifft, die Anwendungen in Java (insgesamt 243 Personen mit Java-Kenntnissen befragt) oder C# (71 Personen mit C# Kenntnissen) programmieren, habe ich diese Werte nochmals besonders hervorgeholt. Die Umfrage fragte nach privaten bzw. in der Firma verwendeten Sprachen. Die Ergebnisse beziehen sich auf die persönlichen Überzeugungen der Programmierer, die nicht durch die Firma vorgegeben wurden. Entsprechend wurden die privat verwendeten, bzw. privat bevorzugten Sprachen abgefragt. Hinter Sprache ist angegeben, wie viele Personen Kenntnisse in dieser Sprache besitzen, in Klammern ist der Prozentsatz der Stimmen angeben, im Verhältnis zu allen, die in der jeweiligen Sprache Kenntnisse besitzen. 418 Stimmen wurden insgesamt gezählt.

Stimmen Wahl C# C#* Java Java*
29 Ich kenne den Begriff Garbage Collection nicht 1 / 8 /
118 Garbage Collection muss sein! 20(28%) 6 84(34%) 56
116 Nützlich, aber entbehrlich 22(31%) 2 78(32%) 35
30 Eigentlich überflüssig 6(8%) 1 9(4%) 2
17 Fehlentwicklung, aufgrund unkontrollierbarer CPU Belastung 3(4%) 1 8(3%) 0
93 Ich komme klar, ob mit oder ohne Garbage Collection 18(25%) 2 52(21%) 19
15 Zu dieser Frage habe ich keine Meinung 1 / 4 /

Die Ergebnisse stellen auf den ersten Blick klar, dass eine Garbage Collection gerne gesehen ist: 234 Stimmen=56% sprechen sich positiv zur Garbage Collection aus, nur 47 Stimmen, also 11%, lehnen die Garbage Collection ab.

Das ist nicht weiter verwunderlich, schließlich stellt die Garbage Collection für den Entwickler eine Vereinfachung der Resourcenverwaltung dar. Mit dem Hintergedanken, dass die Garbage Collection in erster Linie eine Vereinfachung darstellt und niemals eine Notwendigkeit, um ein Programm zu realisieren, darf man die Garbage Collection auch als den Wunsch einer Vereinfachung verstehen.

In diesem Sinne kann man die Ergebnisse mit der Frage nach der Notwendigkeit einer Vereinfachung nochmals lesen: die zweite Antwort drückt Zustimmung zur Garbage Collection aus, der Befragte erklärt damit jedoch auch, dass er auch ohne Garbage Collection bzw. eine derartige Vereinfachung leben kann. Damit erscheint die Hervorhebung der Notwendigkeit zur Vereinfachung (118 Stimmen = 28,5%) im Vergleich zur Neutralität im Bezug auf die Notwendigkeit einer derartigen Vereinfachung (116+93=209 Stimmen, 50%) beachtenswert, bleibt aber in der Minderheit.

50% aller Programmierer sind bereit mit oder ohne Garbage Collection zu programmieren. Allgemein zeigt sich, dass die Sympathien zur Garbage Collection vorrangig von C# und Java-Programmierern gekommen ist. 117 Befragte gaben Java als privat bevorzugte Programmiersprache an, 13 Befragte C#. Am Verhältnis der 130 C# und Java bevorzugenden (31% alle Befragten), machen sich aber 52% der Befragten aus, für die Garbage Collection unbedingt notwendig ist. Hier lässt sich also durchaus schon ein Zusammenhang zwischen der verwendeten Sprache und der persönlichen Notwendigkeit einer Garbage Collection vermuten.

Zwei Dinge lassen sich festhalten: Die Garbage Collection ist als Vereinfachung beliebt: Wer einmal Garbage Collection hatte, weiß das zu schätzen und möchte eine Vereinfachung der Resourcenverwaltung nicht wieder aufgeben.

Die Vielzahl der Nachteile, die eine Garbage Collection mit sich bringt, sowie der Einschätzung, dass eine Garbage Collection ein zu komplexes Thema ist, als dass sie jemals von Microsoft oder Sun ohne die Hilfe von Zusatzhardware in den Griff zu bekommen ist, muss jedoch auch Rechnung getragen werden. Das Aufräumen des Speichers wird immer rechenintensiv bleiben, um diese Prozessorlast zu egalisieren, müsste ein zusätzlicher Prozessor diese Aufgaben übernehmen.
Eine Garbage Collection ist etwas für entsprechend leistungsstarke Rechner. Damit wird eine Vorraussetzung geschaffen, die sich nicht mit einer nicht spezialisierten Sprache vereinbaren lässt, da man sich mit einer Garbage Collection auf leistungsstarke Rechner spezialisieren würde.

Damit fällt an dieser Stelle die Entscheidung, dass der Baustein Garbage Collection nicht empfehlenswert erscheint, obwohl sie dem dringenden und berechtigten Wunsch der automatisierten Resourcenfreigabe entgegenkommt.

Das Loch, das die fehlende Garbage Collection in die der Sprache reißt, gilt es nun wieder zu stopfen. Die Verantwortung über die Resourcenfreigabe wird wieder dem Programmierer zugeschrieben. Es stellt sich die Frage, ob es Möglichkeiten gibt, die Resourcen zu managen, ohne dass auf eine Garbage Collection zurückgegriffen wird.
Zum einen gibt es eine Garbage Collection, die selbst C und C++ ohne Erweiterungen beherscht: Der Aufruf einer Funktion benötigt für die verwendeten lokalen Variablen Speicher und dieser wird nach der Verwendung, also nach Verlassen der Funktion wieder freigegeben. Dies funktioniert aber eben nicht für Referenz-Variablen. Und das ist auch nicht immer sinnvoll, schließlich werden diese Referenzen auch an andere Funktionen übergeben, es können Kopien von Referenzen existieren, deren Ziel nach Verlassen der Funktion bereits freigegeben wurden.
Es bleibt also in der Verantwortung des Programmierers, wann eine Referenzen freigegeben wird.

Trotzdem findet sich häufig ein einfaches Muster, dass sich auch bei den lokalen Variablen: Resource anfordern, verwenden, zurückgeben. Dieses Muster findet sich auch in Klassen: Instanz konstruieren, verwenden und wieder abbauen.

Wie entsteht dann eigentlich der Fall, dass Resourcen verwendet werden, aber dann nicht mehr freigegeben werden?
Es wird Funktionalität aufgerufen, also Funktionen oder der new-Operator, welche Referenzen zurückliefern.

Die Funktion fopen() liefert ein Handle auf eine Datei zurück. Wird nun fclose() nicht mehr aufgerufen, bleibt diese Datei geöffnet. Das Handle existiert aber nicht als eigenständiger Datentyp; es besteht also keine Möglichkeit die Freigabe als lokale Variable zu nutzen.

Es ist weiterhin grundsätzlich eine Referenz, die über fclose() freigegeben werden muss. Das weiß die Sprache allerdings nicht und die einzige Möglichkeit die beiden Funktionen in eine passende Verbindung zu bekommen, ist sie als Konstruktor und Destruktor in eine Klasse zu packen und das Handle selbst dem Programmierer vorzuenthalten. Klassen können problemlos als lokale Variable durch Verlassen der Funktion freigegeben werden. Ein großer Teil dieses Problems entsteht also dadurch, dass Referenzen erzeugende Funktionen keine Verbindung zu der Referenz freigebenden Funktion besitzen. Dies ist nur über Klassen zu realisieren und das ist durchaus eine Betrachtung wert, die Funktionalität für Standard-Resourcen in passenden Datentypen der Sprache unterzubringen.

Hierbei findet sich ein entscheidender Unterschied zwischen C++ und Java: abstrakte Datentypen dürfen in C++ als Value-Types verwendet werden, wohingegen Java alles, das die Komplexität einer Fließkommazahl überschreitet, als Reference-Type behandelt. Der Vorteil von Valuetypes ist die Verwendung also lokale Variable, die bei Eintritt in die Funktion erstellt wird und nach Verlassen der Funktion freigegeben wird.

Damit löst das Problem selbstverständlich auch nur dann, wenn anstelle von Referenzen die Handles als direkte Datentypen verwendet werden können. Aber es ist eine Hilfe für den Programmierer, die Speicherleichen vermeiden hilft. Für diese Form der Resourcenverwaltung kann also auf die Verwaltung der Referenzen bei einer Garbage Collection verzichtet werden, da die Sprache keine Referenzen verwendet: es werden ausschließlich Value-Types verwendet. Da in Java alle abstrakteren Datentypen als Reference-Type abgebildet werden, ist dies so in Java nicht möglich.

Die Möglichkeit Klasseninstanzen wie in C++ als Value-Type zu behandeln, löst hier einige der Probleme auf. Es bleibt die Möglichkeit eine Klasseninstanz auch als Reference-Type zu benutzen, sie muss also mit Hilfe des New-Operators angelegt werden und entsprechend mit delete oder der Garbage Collection wieder beseitigt werden. Dieser Fall liegt nun wieder in der Verantwortung des Programmierers.

Als Fazit dieser Diskussion kommt heraus, dass auch grundlegende Resourcen als Datentyp verstanden werden müssen und die Möglichkeit von Value-Types einen Teil der Garbage Collection auffangen kann. Die Reference-Types verbleiben zunächst in der Verantwortung des Programmierers. Die Hilfestellung für Reference-Types landet auf der Wunschliste, wenn es später darum geht die eigentliche Sprache zu entwerfen.