v03 (Variablen, Date..

Werbung
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
Herunterladen