Vorlesung 3 Inhalt 1 Variablen – Datentypen – Objekte 1.1 Variablen 1.2 Zuweisungen 1.3 Datentyp und Abstrakter Datentyp (ADT) 1.4 Objekte 2 Typisierung 2.1 Starke / schwache Typisierung und statische / dynamische Typisierung 2.2 Typwandlung Reading 2 2 2 4 7 10 11 13 15 V O R L E S U N G 3 : V A R I A B L E N – D A T E N T Y P E N – O B J E K T E Vorlesung 3 Variablen Datentypen Objekte Ziel dieser Vorlesung ist es, weitere zentrale Grundbegriffe der Programmierung kennen und einordnen zu lernen: Variable, Konstante, Literale; Datentyp, abstrakter Datentyp, Objekte und objektorientierte Programmierung. Hinzu gehören die Konzepte Zuweisung und Typing inklusive Typwandlung: casting und coercion. In einem Programm stehen Anweisungen, die auf Daten operieren. Wir benötigen in den Programmiersprachen also Sprachkonstrukte, die es uns erlauben, auf Daten zuzugreifen: sie festzulegen (zu definieren), sie zu lesen. und zu ändern. Dabei soll dieses für den Programmierer einerseits möglichst einfach und bequem sein, quasi so, wie wir es aus der Mathematik oder aus der Schriftsprache gewöhnt sind. Andererseits aber sollen Fehler (insbesondere Tippfehler und manche Denkfehler) möglichst frühzeitig vom Compiler/Interpreter erkannt werden und uns möglichst früh Hinweise darauf geben. Es ist vielleicht nicht einfach einzu- 1 sehen, aber diese Forderungen sind gegenläufig. Sie erfordern ein intensives „Mitdenken“ des Compilers, das z.Z. nicht möglich ist. In der Einführung zur Vorlesung kam die historische Entwicklung der Informatik zur Sprache, die durch mehrere Softwarekrisen ein Instrumentarium zur Fehlervermeidung beim Programmieren aus der Not heraus entwickelt hat. Hierzu gehören die Bereiche der Datentypen und Objekte. In diesem Kapitel soll dieser Bereich, aufbauend auf unseren Erfahrungen mit den Python-Objekten und Datentypen, allgemeiner beleuchtet werden. 1 Variablen – Datentypen – Objekte Daten allein haben keine Aussagekraft – erst zusammen mit einer Interpretationsvorschrift werden hieraus Informationen, die durch den Menschen oder auch den Rechner sinnvoll verarbeitet werden können. Genau diese Idee, die der Interpretationsvorschrift, die zu den Daten gehört, wollen wir aufgreifen um auf jeden Fall eine möglichst genaue syntaktische Prüfung der Zugriffe auf die Daten zu erlauben. Eine vollständige semantische Prüfung ist automatisch allerdings nicht möglich,. Dies ist ein Grund für die Allgegenwärtigkeit von fehlerhaften Programmen. Wir wollen dies kurz historisch aufbauen, wobei diese Einführung nicht auf Details der einzelnen Konzepte eingehen kann, sondern die Zusammenhänge und Prinzipien aufzeigen will. 1.1 Variablen Bis in die fünfziger Jahre hinein, wurden Computer dadurch programmiert, dass der Programmierer in Befehlen direkten Bezug auf Speicherstellen nahm - Adressen benutzte, um auf Daten zuzugreifen. Eine erste Vereinfachung des Programmierens ergab sich dadurch, dass man in den Programmiersprachen • Namen (Bezeichner, Symbole) für einzelne Speicherzellen einführte. Diese (erlaubten) Namen sind Wörter entsprechend einer bestimmten Syntax. Hierfür wurde alsbald der Begriff Variable1 üblich. In Programmiersprachen bezeichnet "Variable" also einen Bezug (Referenz) auf einen (Daten-)Behälter, also einer Speicherzelle. Eine Erweiterung dieses Konzeptes ist es, diesen Namen für ganze Speicherbereiche zu benutzen und auf einzelne Elemente dieses Bereiches durch Indizierung zuzugreifen: X[3] bezeichnet dann das dritte Element des Vektors / der Liste / des Feldes /der Reihung X. 1.2 Zuweisungen Ein nächster Schritt bei der Entwicklung von Programmiersprachen war es, Formeln in Form von Ausdrücken (Termen) anzugeben, und die Übersetzung in ausführbaren Maschinencode einem Compiler zu überlassen. Typisch sind Anweisungen wie Im Unterschied dazu nennt man in der Mathematik eine "Variable" eine Bezeichnung, die in einem Term oder einer Formel vorkommt, z.B. 3u + 2. Sie ist Platzhalter für einen Wert, der an ihrer Stelle eingesetzt werden kann. Eine andere Art der Verwendung ist auch gebräuchlich: bei einem funktionalen Zusammenhang der Art y = f(x) bezeichnet man oft x als unabhängige und y als abhängige Variable. 1 In der Physik bezeichnet man mit "Variable" eine Größe, deren Wert von der Zeit oder einer anderen Größe abhängt. 2 1. X = 4 2. Y = 5 3. X = X/2 + 3*Y Diese Form (diesen Operator) nennt man Zuweisung (assignment). Das Gleichheitszeichen hat hier eine besondere Bedeutung, die anders ist, als in der Mathematik üblich. Die obigen Anweisungen sollen in der Reihenfolge 1. bis 3. ausgeführt werden und bedeuteten: 1. Weise der Variablen (also der Speicherzelle mit der Bezeichnung X) den Wert 4 zu. 2. Weise der Variablen (also der Speicherzelle mit der Bezeichnung Y) den Wert 5 zu. 3. Berechne den Term (Ausdruck) auf der Rechten Seite, also 4/2 + 3*5 gleich 17 und weise den errechneten Wert 17 der Variablen (also der Speicherzelle mit der Bezeichnung X) zu. In der Mathematik würde man diese Schreibweisen als Gleichung auffassen. Die dritte Anweisung als Gleichung aufgefasst würde man auflösen zu X = 6* Y, mit „Gleichung 2.“ : X = 24 (mit „Gleichung 1.“ Y=2/3) Die Punkte 1. – 3., als Gleichungssystem aufgefasst, wären widersprüchlich und die Lösungsmenge für X = {}, die leere Menge. Um diese Verwechslungen zu vermeiden, werden in manchen Programmiersprachen für den Zuweisungsoperator andere Symbole benutzt, z.B. = in Python, Java, C, C++ := in Pascal, Ada ← in APL Die verschiedenen Programmiersprachen unterscheiden sich dadurch, was auf der rechten Seite des Zuweisungsoperators stehen darf. Wenn die Variablen Zahlen sind, dann würde man erwarten, dass rechts ein beliebiger algebraischer Ausdruck (Term) stehen darf und aus Variablen und Zeichen wie + - * / usw. stehen dürfen sowie Klammern ( ), um die Reihenfolge der Auswertung festzulegen, und das sonst die üblichen Regeln, wie „Punktrechnung vor Strichrechnung“ gelten. Genau das wird in Programmiersprachen auch realisiert, genaueres siehe unten. Probleme entstehen nur, wenn in unseren Variablen (den Speicherzellen) ganz andere Informationen stehen, z. B. Zeichen wie X = ‚ANTON’ Y=‚&’ Z = ‚BERTA’. U=X+Y+Z arithmetisch interpretiert macht keinen Sinn, erst recht nicht dieses auf Datenebene plump zu errechnen. Vielmehr könnte der Operator + bei Zeichenketten eine Aneinanderreihung bedeuten, also U = ‚ANTON & BERTA’ 3 Es wird klar, dass die Bedeutung von Operatoren in Ausdrücken Sinnvollerweise stark vom der Art ( = dem Typ, engl. type) der Daten abhängt, die durch die zu verknüpfenden Variablen definiert werden. Auch sind die gleichen Operatoren abhängig vom Datentyp durchaus verschieden. Dieses führt uns direkt zum Begriff des Datentyps. 1.3 Datentyp und Abstrakter Datentyp (ADT) Programmiersprachen bieten eine jeweils spezifische Menge an vordefinierten Datentypen, wie beispielsweise „Ganze Zahlen“ oder „Zeichenketten“ an. Die Namen dieser Datentypen und die genauen Definitionen der Wertebereiche und der dazugehörigen Operationen unterscheiden sich jedoch zum Teil stark. Die Datentypen ermöglichen es einem Compiler, einem Interpreter oder einer Laufzeitumgebung, die Typverträglichkeit der vom Programmierer angegebenen Operationen zu überprüfen. Unzulässige Operationen werden zum Teil bereits beim Kompilieren oder Interpretieren erkannt, so dass beispielsweise die Division einer Zeichenkette ‚HANS’ durch die Zahl ‚5’, was ja nicht sinnvoll und somit undefiniert ist, verhindert wird. Mehr dazu später im Kapitel Typing. Man unterscheidet elementare und zusammengesetzte Datentypen. Elementare (einfache, primitive) Datentypen sind Datentypen einer Programmiersprache, die nicht in einfachere Einheiten zerlegbar sind, also atomar. (ähnlich wie in der Chemie). Sie können oft nur ein Datum des entsprechenden Wertebereichs aufnehmen. Sie besitzen in der Regel eine festgelegte Anzahl von möglichen Werten sowie eine fest definierte Oberund Untergrenze (Endlichkeit). Beispiele sind: • Ganze Zahlen (Übliche Bezeichnungen INTEGER oder INT) • Natürliche Zahlen (Übliche Bezeichnungen NATURAL, UNSIGNED INT oder WORD) • Aufzählungstypen (Übliche Bezeichnung: ENUM oder SET) beispielsweise (ROT, SCHWARZ, GELB) ein besonderer Aufzählungstyp ist • Wahrheitswerte (BOOLEAN) mit dem Wertebereich: (TRUE, FALSE) • Zeichen (Übliche Bezeichnung: CHAR ) mit dem Wertebereich: Alle Elemente des bestimmten Zeichensatzes (zum Beispiel Buchstaben) • Gleitkommazahlen (Übliche Bezeichnung: FLOAT, LONG) • Festkommazahlen (Übliche Bezeichnung: REAL) • Zeigertypen: Eine Besonderheit sind Zeiger, dessen wirklicher Wertebereich in der Regel anonym bleibt, da es 'nur' eine Referenz (Speicheradresse) auf einen anderen beliebigen Datentyp ist. Bezeichnung: ACCESS, POINTER oder auch nur kurz Stern '*'. .Der Nullzeiger (ohne Wert) trägt die Bezeichnung: NULL, VOID oder NIL und hat intern meist einen speziellen Zahlenwert. Benutzt man ihn, so führt dies zu einer „Ausnahme“-Meldung (exception). 4 Jede Programmiersprache stellt eine Auswahl der oben genannten Elementaren Datentypen als „eingebaut“ (builtin) zur Verfügung. Diese betrachten wir im Weiteren in der Vorlesung, insbesondere für Python. Besonders interessant und wichtig sind jedoch die zusammengesetzten Datentypen, die komplexere Strukturen wie Zeichenketten, Listen, Felder, Dictionaries oder allgemeine Strukturen zu behandeln erlauben, das kommt später.. Auch hiervon gibt es in jeder Programmiersprache eine bestimmte Auswahl an builtin. Formal bezeichnet ein Datentyp die Zusammenfassung von Objektmengen mit den darauf definierten Operationen. Dabei werden durch den Datentyp (unter Verwendung einer so genannten Signatur) ausschließlich die Namen dieser Objekt- und Operationsmengen spezifiziert. Ein so spezifizierter Datentyp besitzt jedoch noch keine Semantik. Beispiel Abstrakter Datentyp Signatur Eine Signatur ist ein Paar (Sorten, Operationen), wobei Sorten Namen für Objektmengen und Operationen Namen für Operationen auf diesen Mengen repräsentieren. Ein Beispiel soll dies für eine vereinfachte Version des Datentyps Ganzzahl zeigen, der hier Simple Integer heiße: Simple Integer Sorten int Operationen empty: + : – : End Simple Integer → int (int) × (int) → int (int) × (int) → int Dies ist eine Signatur für einen angenommenen Datentyp Simple Integer, auf dem nur zwei Operationen + und – (neben der "Erzeuger-Operation") erlaubt sind. Die einzige Sorte nennen wir int. Die Operation empty dient zur Erzeugung eines intElementes. Die Operationen + und – sind jeweils zweistellig und liefern jeweils wiederum ein Element der Sorte int. Wichtig ist, dass es sich hier um eine rein syntaktische Spezifikation handelt. Was ein int ist, wird nirgendwo definiert. Hierzu müsste noch eine Zuordnung des Sortennamens zu einer Menge erfolgen. Eine sinnvolle Zuordnung wäre in diesem Fall etwa die Menge der ganzen Zahlen. Auch über die Arbeitsweise der Operationen ist nichts weiter ausgesagt als ihre Stelligkeit und ihr Ergebnis. Ob das +-Symbol der Arbeitsweise der Summenoperation entspricht, wird hier nicht festgelegt - dies wäre auch völlig unmöglich, da nichteinmal bekannt ist, ob die Operation auf den ganzen Zahlen arbeitet. Derartige Zuordnungen fallen in den Bereich der Semantik, die hier nicht spezifiziert ist. Damit wird allerdings der Bereich einer Signatur bereits überschritten. Diese Spezifikation würde man vielmehr als Algebra bezeichnen. Die Spezifikation kommt auf diese Weise jedoch dem programmiersprachlichen Verständnis des Begriffes „Datentyp“ näher. Datentypen werden in der Programmierung verwendet, um Speicherbereichen eine konkrete Syntax und (Teil-)Semantik zuzuweisen. Wenn diese Speicherbereiche veränderlich (mutable) sein sollen, nennt man sie Variablen (s.o.) oder, wenn sie nicht veränderlich sind, Konstanten (constants, unmutable). Direkt in der Programmiersprache angegebene Werte für Operatoren nennt man Literale, z.B. 42, 3.14, ‚Anton’, usw. 5 Ein Abstrakter Datentyp (ADT) ist im Wesentlichen durch eine formale Beschreibung seiner Schnittstelle zur Umwelt charakterisiert. Ein ADT ist eine Erweiterung des Begriffs „Datentyp“ : Die Definition des ADT hält sich dabei an folgendes Muster: 1. Typ/Wertebereich (welche Werte nimmt der ADT an bzw. mit welchen Datentypen geht er um) 2. Methoden - die Syntax, wie mit dem Datentyp gearbeitet wird 3. Axiome - die die Semantik des Datentypen definieren Was bringen ADT's? Nun, sie dienen wieder der Abstraktion. Man kann auf diese Weise einen Datentyp beschreiben ohne sich um die Details der Implementierung zu kümmern, ähnlich wie im Beispiel der Signatur.. Beispiel: Datentyp Termine Ein Terminkalender besteht aus Daten (den Terminen) und Abfragen (Datum frei?) sowie Anweisungen (Setze Datum, lösche Datum etc.). Intern kann der Terminkalender sehr unterschiedlich implementiert werden; nach außen ist er aber immer gleich und dadurch definiert, wie man auf ihn zugreifen kann. Die genaue interne Datenstruktur und internen Methoden (hier: grau schraffiert) sind nach außen hin unbekannt. Datentyp Termin Type Termin Termin_frei(.) setze_Termin(.) lösche_Termin(.) Termin = (Uhrzeit, Tag, Woche, Jahr) … setze_Tag(.) setze_Uhrzeit lösche_Woche Jahr: Integer Uhrzeit: String … Abbildung 1 Beispiel für einen ADT „Termin“. Die nach außen hin sichtbaren Methoden, Attribute und Axiome (Protokolle) bezeichnet man als Schnittstelle (Interface). Man kann es mit dem Schaufenster einer Werkstatt vergleichen, in dem die möglichen Dienste der Werkstatt angepriesen werden, aber das Materiallager und die Maschinen nicht sichtbar sind. Allgemein hat ein solches Vorgehen den Vorteil, dass man beim Entwurf von Software-Systemen beispielsweise die Struktur des Systems vor der Implementierung entwerfen kann. Der Code wird so modularer und gekapselter. Er kann auf diese Art leichter gewartet werden, da er abgesehen von der Signatur ( = Summe der Methoden mit ihren Parametern) unabhängig vom Rest des Softwaresystems implementiert werden kann. 6 Die anzustrebenden, positiven Eigenschaften eines gut programmierten ADT sind auch die einer gut spezifizierten Datenstruktur: • Universalität (implementation independence): Der einmal entworfene und implementierte ADT kann in jedes beliebige Programm einbezogen und dort benutzt werden (z.B. in Form einer Unit). • Präzise Beschreibung (precise specification): Die Schnittstelle zwischen Interface und Implementation muss eindeutig und vollständig sein. • Einfachheit (simplicity): Die Benutzung des ADT wird einfacher; der Anwender muss sich nicht um die interne Realisierung des ADT kümmern, da der ADT seine Repräsentation und Verwaltung im Speicher selbst übernimmt. • Kapselung (encapsulation): Es ist nicht nur unnötig, die Implementierung zu kennen, sondern auch schädlich: Die Benutzung des ADT wird sonst mit diesem Wissen optimiert und bei einer internen Änderung des ADT ist die Benutzung dann nicht mehr optimal. Deshalb: Das Interface soll als eine hermetische Grenze aufgefasst werden. Der Anwender soll sehr genau wissen, was ein ADT tut, aber keinesfalls, wie er es tut. • Geschütztheit (integrity): Der Anwender kann in die interne Struktur der Daten nicht eingreifen. Die Gefahr, Daten ungewollt zu löschen bzw. zu verändern sowie Programmierfehler zu begehen, ist dadurch deutlich herabgesetzt. • Modularität (modularity): Module bilden heißt, das Problem in von einander unabhängige Unterprobleme zu unterteilen. Das modulare Prinzip erlaubt übersichtliches und damit sicheres Programmieren und einen einfacheren Austausch von Programmteilen. Bei der Fehlersuche können einzelne Module isoliert betrachtet werden. Viele Verbesserungen können über ADTs nachträglich ohne die geringste Änderung in sämtlichen Umgebungs- bzw. Anwendungsprogrammen übernommen werden. Wird objektorientiert programmiert, können diese Eigenschaften besonders leicht erfüllt werden, weil das objektorientierte Paradigma auf natürliche Weise die Erstellung von ADTs unterstützt. 1.4 Objekte Nun kommen wir zum Begriff Objekt im Kontext der „Objektorientierten Programmierung“ (Abkürzung OOP). Man versteht darunter die Idee, nicht die Anweisungen in den Vordergrund zu stellen, die auf Daten arbeiten, sondern umgekehrt die Daten zuerst zu definieren und dann die darauf operierenden Anweisungen. Beides, Daten und zugehörigen Anweisungen, werden zu Objekten zusammengefasst, siehe Abbildung 1. Aus der Blickrichtung der Datentypen ist OOP damit eine Erweiterung der ADT, bei der zusammengehörige Daten und die darauf arbeitende Programmlogik zu Einheiten zusammengefasst werden. In Abbildung 2 ist dies verdeutlicht. 7 Abbildung 2 Konventionelle (links) und objekt-orientierte (rechts) Programmierung. OOP schließt aber auch ein Verfahren zur Strukturierung von Computerprogrammen mit ein: Zumindest konzeptionell arbeitet ein Programm dann nicht mehr (wie bei der imperativen/ prozeduralen, anweisungsorientierten Programmierung so, dass sequenziell einzelne Schritte eines Programms durchlaufen werden, das dabei eine Anzahl Daten verändert, sondern die Programmlogik entfaltet sich in der Kommunikation und den internen Zustandsveränderungen der Objekte, aus denen das Programm aufgebaut ist. Vorteile der objektorientierten Programmierung liegen in der besseren Modularisierung des Codes, dadurch bedingt in einer höheren Wartbarkeit und Wiederverwendbarkeit der Einzelmodule, sowie in einer höheren Flexibilität des Programms insgesamt, insbesondere in Bezug auf die Benutzerführung, da Programme dieser Art weniger stark gezwungen sind, dem Benutzer bestimmte Bedienabläufe aufzuzwingen. Im Folgenden werden wichtige Begriffe der objektorientierten Programmierung kurz umrissen. Die einzelnen Bausteine, aus denen ein objektorientiertes Programm während seiner Abarbeitung besteht, werden, wie bereits erwähnt, als Objekte bezeichnet. Die Konzeption dieser Objekte erfolgt dabei in der Regel auf Basis der folgenden Paradigmen: • Abstraktion: Jedes Objekt im System kann als ein abstraktes Modell eines Akteurs betrachtet werden, der Aufträge erledigen, seinen Zustand berichten und ändern und mit den anderen Objekten im System kommunizieren kann, ohne offen legen zu müssen, wie diese Fähigkeiten implementiert sind (vgl. abstrakter Datentyp (ADT)). • Kapselung: Objekte können den internen Zustand anderer Objekte nicht in unerwarteter Weise lesen oder ändern. Ein Objekt hat eine Schnittstelle, die darüber bestimmt, auf welche Weise mit dem Objekt interagiert werden kann. • Polymorphie: Verschiedene Objekte können auf die gleiche Nachricht unterschiedlich reagieren. Wird die Zuordnung einer Nachricht zur Reaktion auf die Nachricht erst zur Laufzeit aufgelöst, dann wird dies auch späte Bindung (oder dynamische Bindung) genannt. • Vererbung: Neue Arten von Objekten können auf der Basis bereits vorhandener Objekt-Definitionen festgelegt werden. Es können neue Bestandteile hinzugenommen 8 werden oder vorhandene überlagert werden. Wird nur keine Vererbung zugelassen (die sonstigen Eigenschaften aber erfüllt), so spricht man zur Unterscheidung oft auch von objektbasierter Programmierung. • Klassen: Zur einfacheren Verwaltung gleichartiger Objekte bedienen sich die meisten Programmiersprachen des Konzeptes der Klasse. Klassen sind Vorlagen, aus denen Objekte (Instanzen) zur Laufzeit erzeugt werden. Im Programm werden dann nicht einzelne Objekte, sondern eine Klasse gleichartiger Objekte definiert. Klassen sind die Konstruktionspläne für Objekte. Die Klasse entspricht in etwa einem Datentyp wie in der prozeduralen Programmierung, geht aber darüber hinaus: Sie legt nicht nur den Datentypen fest, aus denen die mit Hilfe der Klassen erzeugten Objekte bestehen, sie definiert zudem die Algorithmen, die auf diesen Daten operieren. Während also zur Laufzeit eines Programms einzelne Objekte miteinander interagieren, wird das Grundmuster dieser Interaktion durch die Definition der einzelnen Klassen festgelegt. • Methoden: Die einer Klasse von Objekten zugeordneten Algorithmen bezeichnet man auch als Methoden. Häufig wird der Begriff Methode synonym zu Funktion oder Prozedur oder Routine gebraucht, obwohl eine Funktion, Routine oder Prozedur eher als Implementierung einer Methode zu betrachten ist. Im täglichen Sprachgebrauch sagt man "Objekt A ruft Methode m von Objekt B auf." Spezielle Methoden zur Erzeugung bzw. "Zerstörung" (Speicherfreigabe) von Objekten heißen Konstruktoren und Destruktoren. In verschiedenen Programmiersprachen ist die Terminologie leider nicht einheitlich. Folgende Bezeichnungen werden synonym verwendet: Superklasse = Basisklasse = Oberklasse Subklasse = abgeleitete Klasse = Unterklasse Methode = Elementfunktion = Memberfunktion Attribut = Datenelement = Member (aus einer Klasse erzeugtes) Objekt = Exemplar = Instanz Prinzipiell kann man durch strikte Einhaltung bestimmter Regeln in den meisten Programmiersprachen objektorientiert programmieren. Jedoch erleichtern und fördern speziell hierfür ausgerichtete objektorientierte Programmiersprachen dies ungemein. Die meisten objektorientierten Programmiersprachen erlauben es, verschiedene Programmiertechniken miteinander zu kombinieren. Manchmal werden dabei bestimmte Prinzipien der objektorientierten Programmierung durchbrochen. Beispielsweise handhaben viele Programmiersprachen das Prinzip der Kapselung nicht ganz so streng und stellen es dem Entwickler anheim, wie stark er bestimmte Konzepte wie beispielsweise die Kapselung objektinterner Daten durch Zugangsmethoden einhält oder nicht. In rein objektorientierten Sprachen wie Smalltalk und auch in Python werden dem Prinzip alles ist ein Objekt folgend, auch elementare Typen wie Ganzzahlen (Integer) durch Objekte repräsentiert. Auch Klassen selbst sind hier Objekte, die wiederum Ausprägungen von Metaklassen sind. Viele Sprachen, unter anderem C++ und Java folgen allerdings nicht der „rei- 9 nen Lehre“ der Objektorientierung; daher sind dort elementare Typen keine vollwertigen Objekte, sondern müssen auf Methoden und Struktur verzichten. So wie die Techniken der prozeduralen Programmierung durch Verfahren wie die strukturierte Programmierung verfeinert wurden, so gibt es inzwischen auch Verfeinerungen der objektorientierten Programmierung durch Methoden wie Entwurfsmuster (englisch design patterns), Design by Contract (DBC) und grafische Modellierungssprachen wie UML. Eine relativ neue Entwicklung ist die aspektorientierte Programmierung, bei dem Aspekte von Eigenschaften und Abhängigkeiten beschrieben werden. Zusammenfassung: Ziel ist es, Vereinfachungen für den Programmierer bei der Programmerstellung, Wartung und Fehlersuche zu realisieren. Dabei entwickelten sich die Ideen, wobei Teilkonzepte der Abstrakten Datentypen deutlich früher entwickelt wurden als die Objektorientierung. Als zugehörig sind hierbei die Konzepte im Bereich des Kontrollflusses zu betrachten. Diese behandeln wir später. Zusammenfassung der Ideen und Konzepte Erste Softwarekrise Ende 60er Zweite Softwarekrise Anfang 80er Adressen Variablen Datentypen Konstanten Literale abstrakte Datentypen (Theorie) Objekte (Praxis) (Bezeichner Namen, Symbole) Syntax Polymorphie Semantik Abstraktion Kapselung Klassen Vererbung 2 Typisierung Eine Typisierung (engl. typing) dient dazu, dass die Elemente und Einheiten der Programmiersprachen, wie z.B. Variablen, Funktionen oder Objekte (im Sinne der Objektorientierten Programmierung) nur „korrekt“ verwendet werden können. Ziel ist es, Programmierfehler der Art „5 + ‚Anna’“ so früh wie möglich zu erkennen, z.B. schon beim Eintippen in einen Syntaxgesteuerten Editor, im Compiler/Interpreter oder durch das Laufzeitsystem abzufangen; insbesondere um eine Verschleppung von Laufzeitfehlern zu vermeiden. Diese „verschleppten“ Fehler sind oft sehr schwer zu finden. Ein umgangssprachliches Beispiel für die dahinter liegende Problematik ist, dass man nicht Äpfel mit Birnen vergleichen soll, weil dabei nur Fehlschlüsse entstehen können. 10 2.1 Starke / schwache Typisierung und statische / dynamische Typisierung Ein bewährtes Hilfsmittel. um Programmierfehler zu entdecken, ist die Benutzung von Datentypen anstelle der „rohen“ Zahlenwerte. Typen liefern Bedingungen, deren Einhaltung bzw. Verletzung bei der Übersetzung oder späteren Ausführung vom Typsystem kontrolliert werden kann und somit eine Maßnahme gegen Programmierfehler darstellt. Man unterscheidet dabei: • starke Typisierung (strong typing) - schwache Typisierung (weak typing) • dynamische Typisierung (dynamic typing) - statische Typisierung (static typing) Bei der starken Typisierung (oder strengen Typisierung) bleibt eine einmal durchgeführte Bindung zwischen Variable und Datentyp bestehen; es wird auch (möglichst) keine implizite Typkonvertierung vorgenommen. Eine nicht stark typisierte Sprache bezeichnet man als schwach typisiert. Leider ist das Konzept des strong typing alles andere als eindeutig. In der Literatur finden sich diverse Regeln, die sich teilweise sogar widersprechen. Versucht man alle bisher in der Literatur aufgestellten Regeln für strong typing auf bekannte Programmiersprachen anzuwenden, hält keine Sprache dieser Überprüfung stand. Eine Sprache ist stark typisiert, wenn • sie Typüberprüfungen zur Compile-Zeit enthält (erfüllbar nur bei Compilern, nicht bei Interpretern) • Typkonvertierungen generell verboten sind; • Typkonvertierungen explizit durchgeführt werden müssen; • die Sprache keine Mechanismen besitzt, um das Typ-System zu übergehen, wie etwa type casts (Typumwandlungen) in C; • es ein komplexes, fein abgestuftes System an Typen mit Sub-Typen gibt; • das Typ-System das Laufzeitverhalten eines Programms garantieren kann. Vorteile der starken Typisierung: Der Compiler/Interpreter kennt zu jeder Zeit den Typ eines Wertes (Datums) im Speicher, d.h. (1) Typfehler können entweder zur Compilezeit, spätestens beim Binden erkannt werden oder werden durch einen Interpreter abgefangen (2) Compiler erzeugt performanteren Code, weil Typprüfungen zur Laufzeit nicht nötig sind! Nachteile (1) Variablen bleiben während der Laufzeit bezüglich Typ und Größe unveränderlich (insbesondere auch die Größe eines zusammengesetzten Datentyps), (2) die Übersetzer sind aufwendiger, weil dort mehr Aufwand für die Analyse anfällt und 11 (3) viele „effiziente“ Programmiertricks auf Datenebene (z.B. ändere einen kleinen Buchstaben „a“ in einen großen „A“, durch Subtraktion von 32 (im ASCII Alphabet) sind nicht möglich. Beispiele Stark typisierter Sprachen (keine der genannten Sprachen genügt allerdings allen Definitionen.): • Java, Python, Pascal Schwach typisierter Sprachen sind • C / C++, PHP, Perl, JavaScript Bei der dynamischen Typisierung (engl. dynamic typing) erfolgt die Typzuweisung der Variablen zur Laufzeit eines Programms durch das Laufzeitsystem, z.B. einer virtuellen Maschine. Dies erspart es dem Entwickler, die Typisierung „von Hand“ vorzunehmen, bringt aber gewisse Nachteile für die Performance und bei der Fehlersuche mit sich. Bei der statischen Typisierung muss zur Übersetzungszeit der Datentyp von Variablen bekannt sein. Dies erfolgt in der Regel durch Deklaration. Unter Deklaration versteht man die Festlegung von Dimension (Anzahl der Unterelemente wie in Listen), Bezeichner, Datentyp und weiteren Aspekten einer Variablen (oder eines Unterprogramms, s. später). Durch die Deklaration wird dem Compiler oder Interpreter diese Variable (bzw. dieses Unterprogramm) bekannt gemacht; es ist damit zulässig, diese an anderen Stellen im selben Quelltext zu verwenden. Häufig werden die Begriffe Deklaration und Definition gleichgesetzt. Streng genommen ist Definition allerdings ein Sonderfall der Deklaration. Bei Variablen spricht man von Definition, wenn der Compiler Code erzeugt, der entweder statisch (im Datensegment) oder dynamisch (zur Laufzeit) Speicherplatz für diese Variable reserviert. Bei Unterprogrammen spricht man von Definition, wenn an dieser Stelle der Quelltext des Unterprogramms angegeben ist, der vom Compiler übersetzt wird. Die Deklaration eines Unterprogramms ohne Definition wird auch oft als Prototyp bezeichnet. Das folgende Beispiel deklariert und definiert die Variable x mit dem Datentyp Integer. int x; INTEGER x; // in C, C++, Java ! in FORTRAN Im folgenden Beispiel bewirkt das Schlüsselwort extern, dass die Variable x nur deklariert, nicht definiert wird. Die Definition muss an einer anderen Stelle in derselben oder einer anderen Quelltext-Datei erfolgen. extern int x; in C, C++, Java Bei einer Deklaration ohne Definition überprüft der Binder (Linker), dass die Variable bzw. das Unterprogramm an anderer Stelle definiert wurde und verknüpft die Deklarationen miteinander. Neben der expliziten Deklaration gibt es (eigentlich nur noch historisch bedeutsam) in einigen Programmiersprachen (z.B. Fortran, Basic, PL/1) auch die Möglichkeit einer impliziten Deklaration von Variablen, z.B. alle Variablen, deren Bezeichner mit i,j,k,l,m,n beginnen sind vom Typ Integer. In diesem Fall führt das erste Auftreten eines Variablennamens zu einer automatischen Typzuordnung. 12 Vorteile der dynamischen Typisierung: (1) Auswahl des Operators wird zur Laufzeit entschieden, einfaches Operator overloading und einfacheres → "Generic Programming", (2) Wesentlich kürzere Compile-Zeiten, weil viele Überprüfungen entfallen; (3) Variablen müssen nicht deklariert werden = Bequemlichkeit für den Programmierer (4) Variable muss nicht an festen Speicherbereich gebunden werden Nachteile: (1) Der Typ von Variable/Wert wird zur Laufzeit (jedes Mal) überprüft ⇒ die Werte im Speicher müssen (unveränderlichen) Typen-Kennzeichnung (tag) haben, das System hat geringere Performance (langsamer!) und es benötigt mehr Speicherplatz. (2) Der Debugger benötigt wesentlich höhere Funktionalität. Zusammenfassend ist festzuhalten: Das static vs. dynamic typing und das strong vs. weak typing sind unabhängig (orthogonal ) zueinander. Die folgende Abbildung gibt die prinzipiellen Zusammenhänge wieder. Abbildung 3: Einordnung einiger Programmiersprachen bzgl. ihres Typings WARNUNG: Leider findet man häufig einen falscher Sprachgebrauch – oder vielleicht auch Unverständnis über diese Konzepte: FALSCH ist: "strong" = "static und strong", oder gar: "strong" = "static", "C = strongly typed" ...). 2.2 Typwandlung Man kann darüber streiten, ob Typkonvertierungen (-wandlungen) überhaupt sinnvoll sind. In den meisten Programmiersprachen werden sie in „hoffentlich“ wohlkontrollierter Form aus folgenden Gründen zugelassen: aus der Mathematik sind wir es gewohnt, dass wir ganze Zahlen zu reellen Zahlen addieren können. Das ist auch sinnvoll, da ganze Zahlen ja eine Teilmenge der reellen Zahlen sind. Implizit unterstellen wir dann, dass das Ergebnis eine reelle Zahl ist. Diese „Selbstverständlichkeit“ sollte auch in Programmiersprachen abgebildet sein, so ein Argument für eine Typkonvertierung. Wir unterscheiden in Programmiersprachen grundsätzlich zwei Arten von Typkonvertierungen: 13 (1) implizite Typkonvertierung oder coercion (engl. Nötigung, Zwang) (2) explizite Typkonvertierung oder cast(ing) (engl eingießen, formen, werfen, …) Die erste Möglichkeit der impliziten Typkonvertierung finden wir sehr häufig bei Zahlen, wie das obige Beispiel es nahe legt. Dabei ist eine Regel unterlegt, dass wenn zwei nichtgleiche Zahlentypen miteinander verknüpft werden sollen, zunächst zum allgemeineren (höheren) Typ gewandelt wird, also z.B. eine natürliche Zahl → ganze Zahl → reelle Zahl → komplexe Zahl gewandelt. Dies kann implizit geschehen, weil Verwechslungen kaum möglich sind und auch keine Informationen verloren gehen Fraglich wäre dies aber ggf. in folgendem Fall a ist eine ganze Zahl (integer), z.B. 4 b ist eine Zeichenkette (string), z.B. ’22’ welchen Typ hat dann a + b ? - Für ganze Zahlen wäre a + b die Addition und das Ergebnis eine ganze Zahl, - für Zeichenketten wäre a + b die Konkatenation und das Ergebnis ’422’. In diesen Fällen ist die Typwandlung nicht mehr durch allgemein übliche Konventionen gedeckt und in der Regel auch in Programmiersprachen nicht realisiert. Bei den expliziten Typwandlungen kann man drei Arten unterscheiden: • checked: es wird zur Laufzeit überprüft, ob der Zieltyp den „mächtiger“ ist als der Quelltyp • unchecked: keine Typüberprüfung zur Laufzeit, ggf. generiert aber die Hardware eine Fehlermeldung • bit pattern: Daten werden in keiner Weise überprüft, das Bitmuster wird uminterpretiert. Jede Programmiersprache hat dabei ihren eigenen Regelsatz. In Ada werden die drei o.g. Arten unterstützt, in C/C++ ist ein cast entweder unchecked oder bit pattern. Häufig gehen dabei Informationen verloren; z.B. 1,3 wird zu 1. Gerade C++ hat diesbezüglich mehrere verschiedene Cast Operatoren: static_cast<type>(value_to_cast) dynamic_cast<type>(value_to_cast) const_cast<type>(value_to_cast) reinterpret_cast<type>(value_to_cast) Wir sehen hieraus, dass Typkonvertierung ein mächtiges aber durchaus nicht unproblematischesVerfahren ist. Wir werden das Problem der Typkonvertierung insbesondere an unserer Beispielsprache Python noch mehrfach diskutieren. 14 Reading Die Frage, warum wir als Einführungssprache Python benutzen, stellt sich natürlich immer wieder. Im folgenden Reading finden Sie hierzu Argumente und Anregungen: Python Advocacy HOWTO, A.M. Kuchling, Quelle http://www.amk.ca/python/howto/advocacy/ . Dies ist auch in der Materialsammlung zu finden. 15