Programmiersprachen und Übersetzer

Werbung
Programmiersprachen und Übersetzer
Sommersemester 2011
10. Juli 2011
Einführung in die objektorientierte Programmierung
I
Einführung 1967 durch die Sprache Simula 67 von O.J. Dahl
und K. Nygaard. In dieser Sprache wurden erstmalig Klassen
und Objekte eingeführt.
I
Ende der 60er Jahre entstand im Rahmen des
Dynabook-Projekts am Palo Alto Research Center von Xerox
unter der Leitung von Alan Kay die Programmiersprache
Smalltalk — eine rein objektorientierte Programmiersprache.
I
Die erste Version war Smalltalk 72 (1972), die letzte, auch
heute noch aktuelle Version ist Smalltalk 80
(Adele Goldberg, 1980).
Wichtige Konzepte in Smalltalk
Das gesamte Smalltalk-Environment ist in Smalltalk geschrieben.
Der Quellcode des gesamten Systems steht zur Verfügung und
kann gelesen und bei Bedarf geändert werden.
Wichtige Punkte:
I
Bitmap-Graphik
I
Verwendung der Maus als Eingabegerät
I
Fenstersystem mit allgemeiner Menüsteuerung
I
Byte-Code für eine virtuelle Maschine
I
automatisches Garbage Collection
I
inkrementelle Programmentwicklung
Charakteristika objektorientierter Sprachen:
1. die dynamische Auswahl der Methoden (dynamic lookup).
2. die Abstraktion (abstraction) vom internen Aufbau der
Objekte.
3. die Möglichkeit zur Bildung von Untertypen (subtyping).
4. die Vererbung (inheritance) von Eigenschaften
Objekte
I
Objekte sind zur Laufzeit in einem objektorientierten System
existierende Einheiten. Sie belegen Speicherplatz und haben
daher eine assoziierte Adresse.
I
Ein Objekt hat einen zugeordneten Datenbereich, in dem der
momentane Zustand des Objekts dargestellt wird. Dieser
Zustand wird durch Eigenschaften (Attribute) des Objekts
repräsentiert. Die momentanen Werte dieser Eigenschaften
sind in lokalen Variablen des Objekts, den sogenannten
Instanzenvariablen, gespeichert.
I
Einem Objekt sind Funktionen, sogenannte Methoden
(Aktionen) zugeordnet, die das Objekt beherrscht“ und
”
ausführen kann.
I
Abstraktionsbarrieren kapseln den Zustand eines Objekts ein.
Der Zustand (Werte der Instanzenvariablen) kann
üblicherweise nur über eine der zugeordneten Methoden
verändert oder nach außen hin sichtbar gemacht werden
(Prinzip des Information Hiding).
I
Um eine Methode auf ein Objekt anzuwenden, wird in
objektorientierten Systemen häufig ein Nachrichtenschema
verwendet. Man schickt einem Objekt eine Nachricht. Dies
führt dann dazu, dass eine der Methoden ausgewählt und auf
das Objekt angewendet wird.
Klassen
I
Eine Klasse definiert eine Menge möglicher, gleichartiger
Objekte (Instanzen der Klasse), die alle die gleichen
Attribute und Methoden besitzen. Eine Klasse kann als ein
vom Benutzer definierter Datentyp interpretiert werden
(abstrakter Datentyp).
I
Für jede Klasse existiert ein Mechanismus, der es gestattet,
Objekte der Klasse zu erzeugen und wieder zu vernichten. In
vielen Systemen geschieht dies dynamisch, also zur Laufzeit
des Systems. Häufig wird auch ein Garbage Collector
verwendet, um den frei gewordenen Speicher zu sammeln
I
Es gibt sogenannte abstrakte Klassen, von denen keine
Objekte erzeugt werden. Sie dienen in Verbindung mit der
Vererbung im wesentlichen nur zur Sammlung gemeinsamer
Methoden und Attribute.
Dynamische Auswahl der Methoden
I
Wenn einem Objekt eine Nachricht geschickt wird, dann
entscheidet das Objekt, welche Methode ausgewählt wird.
I
Unterschiedliche Objekte, die etwa nacheinander durch eine
Variable x repräsentiert werden, reagieren auf die selbe
Nachricht unterschiedlich.
I
Wichtig daran ist, dass die auszuführende Methode
dynamisch, also zur Laufzeit des Programms, gewählt werden
kann.
Beispiel
Es sollen Funktionen implementiert werden, die für alle
Angehörigen der Universität Informationen darstellen oder das
Gehalt auszahlen. In konventioneller Form würde man zwei
Funktionen etwa der folgenden Form schreiben:
info(x) =
case type(x) of
Professor:
Assistent:
Hilfskraft:
end;
end;
["Display Info über Professor"];
["Display Info über Assistent"];
["Display Info über Hilfskraft"];
und
bezahle(x) =
case type(x) of
Professor:
Assistent:
Hilfskraft:
end;
end;
["zahle Professor"];
["zahle Assistent weniger"];
["zahle Hilfskraft viel weniger"];
In einem objektorientierten Programm sind die Funktionen an die
Daten gekoppelt, auf die sie angewendet werden.
In unserem Beispiel hätten wir die Klasse der Professoren, der
Assistenten und der Hilfskräfte und jede Klasse würde die zwei
Methoden info und bezahle enthalten. Also etwa:
class Professor =
info =
["Display Info über Professor"];
bezahle = ["zahle Professor"];
end;
und
class Assistent =
info =
["Display Info über Assistent"];
bezahle = ["zahle Assistent weniger"];
end;
class Hilskraft =
info =
["Display Info über Hilfskraft"];
bezahle = ["zahle Hilfskraft viel weniger"];
end;
Vergleich funktionale - prozedurale Vorgehensweise:
Operation
info
bezahle
Professor
info Professor
bezahle Professor
Assistent
info Assistent
bezahle Assistent
Hilfskraft
info Hilfskraft
bezahle Hilfskraft
I
In konventionellen Programmiersprachen wird der Code
zeilenweise in einer Funktion gruppiert, die auf allen hier
auftretenden Arten von Daten arbeitet.
I
In objektorientierten Programmiersprachen wird der Code
spaltenweise gebündelt, in dem die einzelnen Funktionsteile
mit den Daten, auf denen sie arbeiten sollen, gruppiert werden.
Abstraktion
In diesem Kontext bedeutet Abstraktion das Verstecken von
Implementationsdetails einer Programmeinheit, so dass auf die
Interna nur über ein spezielles Interface zugegriffen werden kann.
Beispiel
Man stelle sich die folgenden zwei abstrakten Datentypen für
Warteschlangen (queue) und für Prioritätswarteschlangen (priority
queue) vor.
(hier: ML-Notation für abstrakte Datentypen. Der Einfachheit
halber sollen die abgespeicherten Objekte Integer-Zahlen sein.)
Zunächst die Definition einer Warteschlange. Sie wird als Liste mit
den üblichen Operationen dargestellt.
exception Empty;
abstype queue = Q of int list
with
fun mk Queue() = Q(nil)
and is empty(Q(l)) = l=nil
and add(x,Q(l)) = Q(l@[x])
and first(Q(nil)) = raise Empty | first(Q(x::l)) = x
and rest(Q(nil)) = raise Empty | rest(Q(x::l)) = Q(l)
and length(Q(nil)) = 0 | length(Q(x::l)) = 1+ length(Q(l))
end;
Die Prioritätswarteschlange ist eine Warteschlange, in der beim
Entfernen eines Elementes immer das kleinste ausgewählt wird.
Im Beispiel wird das dadurch erreicht, dass die Objekte in der Liste
immer aufsteigend sortiert gehalten werden.
abstype pqueue = Q of int list
with
fun mk PQueue() = Q(nil)
and is empty(Q(l)) = l=nil
and add(x,Q(l)) =
let fun insert(x,nil) = [x:int]
|
insert(x,y::l) =
if x<y then x::y::l else y::insert(x,l)
in Q(insert(x,l)) end
and first(Q(nil)) = raise Empty | first(Q(x::l)) = x
and rest(Q(nil)) = raise Empty | rest(Q(x::l)) = Q(l)
and length(Q(nil)) = 0 | length(Q(x::l)) = 1+ length(Q(l))
end;
I
Als Interface eines abstrakten Datentyps (die Signatur) wird
üblicherweise die Liste der öffentlichen Funktionen und deren
Typen bezeichnet.
I
In unserem Beispiel sind beide Interfaces bis auf die
Typ-Namen queue und pqueue identisch.
I
In konventionellen Programmiersprachen kann diese
Korrespondenz bei abstrakten Datentypen nicht ausgenutzt
werden.
I
Fünf Funktionen haben eine identische Implementation!
I
In objektorientierten Programmiersprachen kann man den
Vererbungsmechanismus benutzen, um etwa
Prioritäts-Warteschlangen aus der Definition der
Warteschlangen durch Umdefinition der add-Funktion und
Mitbenutzung der anderen Funktionen zu definieren.
Untertypen
I
Das Bilden von Untertypen erzeugt eine Relation auf den
Typen, die es erlaubt, Werte eines Typs anstelle von Werten
eines anderen Typs zu benutzen.
I
Ist X Untertyp von Y , geschrieben X <: Y , dann kann jeder
Ausdruck vom Typ X ohne Hervorrufen eines Typ-Fehlers in
jedem Kontext benutzt werden, in dem ein Ausdruck vom Typ
Y benötigt wird.
I
Das Konzept der Untertypen erlaubt eine konsistente
Behandlung heterogen zusammengesetzter Daten, die alle
Untertyp eines gemeinsamen Typs sind.
Vererbung
I
Vererbung ist ein Konzept, das es erlaubt, neue Objekte durch
Erweiterung bereits existierender Objekte zu definieren.
I
Durch Vererbung werden Attribute und Methoden einer
Klasse X an eine andere Klasse Y weitergegeben. Die Klasse Y
ist dann eine Unterklasse von X bzw. die Klasse X ist
Oberklasse der Klasse Y.
I
In einer Unterklasse können Attribute und Methoden zu den
geerbten der Oberklasse hinzugefügt werden. Es können aber
auch vererbte Methoden neu definiert und/oder implementiert
werden.
I
Bei einer einfachen Vererbung hat jede Klasse höchstens eine
Oberklasse. Dies führt zu einer hierarchischen Anordnung der
Klassen.
I
Ist eine mehrfache Vererbung erlaubt, so hat eine Klasse
mehrere Oberklassen.
(Aber Zyklen und Namenskonflikte möglich!)
I
Vom Prinzip her könnte man Vererbung durch Duplizieren von
Code realisieren.
Bemerkung
Wichtig ist in diesem Zusammenhang der Unterschied zwischen
Untertyp-Bildung und Vererbung. Untertypen bilden eine Relation
auf den Typen (Interfaces), Vererbung dagegen bildet eine Relation
auf den Implementationen.
Beispiel
Es soll ein Programm geschrieben werden, das mit den
Datenstrukturen stack, queue und dequeue arbeitet.
stack: Eine Datenstruktur mit Einsetz- und Löschoperation,
so dass das zuerst eingesetzte Objekt als letztes
entfernt wird (first-in, last-out).
queue: Eine Datenstruktur mit Einsetz- und Löschoperation,
so dass das zuerst eingesetzte Objekt als erstes
entfernt wird (first-in, first-out).
dequeue: Eine Datenstruktur mit zwei Einsetz- und zwei
Löschoperationen. Eine dequeue ist eine Liste, bei der
sowohl am Anfang als auch am Ende Objekte
eingesetzt oder entfernt werden können.
I
Die Datenstruktur dequeue kann sowohl die Aufgaben der
Datenstrukturen stack als auch queue übernehmen, also
kann man zunächst die Klasse dequeue implementieren und
dann die Klassen stack und queue als Unterklassen von
dequeue definieren, indem man die geerbten Methoden zum
Einsetzen und Löschen umbenennt bzw. überschreibt.
I
Obwohl stack und queue Unterklassen von dequeue sind,
bilden sie keinen Untertyp von dequeue.
I
Dagegen kann man in jedem Kontext, in dem man etwa ein
Objekt der Klasse stack bzw. queue benutzt, ohne
Schwierigkeiten auch ein Objekt der Klasse dequeue
benutzen. Folglich ist dequeue Untertyp sowohl von stack
als auch von queue.
Kurze Einführung in Smalltalk
I
In Smalltalk ist alles ein Objekt.
I
es gibt keine primitiven Typen, keine inneren Klassen usw.
I
Bindungen geschehen nur über Referenzen
I
jedes Objekt ist Instanz einer Klasse
I
jede Klasse ist ein Objekt, also Instanz einer anderen Klasse
I
alle Methoden sind public
I
es gibt nur einfache Vererbung
I
jede Berechnung geschieht über das Senden von Nachrichten
Nachrichten in Smalltalk
Es gibt in Smalltalk drei verschiedene syntaktische Formen für
Nachrichten, die an ein Objekt geschickt werden können. Jede
Nachricht besteht aus einem Selektor ( Name“ der Nachricht) und
”
eventuellen Argumenten. Der Selektor bestimmt die anzuwendende
Methode.
1. empfänger unäreNachricht
Die Nachricht besteht nur aus einem Selektor.
2. empfänger binäreNachricht
Dabei besteht der Selektor einer binären Nachricht aus einem
oder zwei speziellen Zeichen (wie etwa +, -, *, /, //, <=
usw.), und das eine Argument folgt dem Selektor.
3. empfänger schlüsselwortNachricht
Die Nachricht besteht aus einem oder mehreren
Schlüsselworten, die jeweils mit einem Doppelpunkt enden.
Hinter jedem Schlüsselwort steht ein Argument.
Zusammengesetzte Nachrichten werden in folgender Reihenfolge
abgearbeitet:
1. geklammerte Nachrichten
2. unäre Nachrichten
(linksassoziativ!)
3. binäre Nachrichten
(linksassoziativ!)
4. Schlüsselwort-Nachrichten
I
Variablen müssen vor ihrem Gebrauch deklariert werden. In
Smalltalk ist einer Variablen im Gegensatz zu üblichen“
”
Programmiersprachen kein Typ zugeordnet!
I
Das Binden eines Objekts an eine Variable geschieht über das
Wertzuweisungszeichen (:=).
I
Ein vorangestelltes ˆ-Zeichen wirkt wie eine return-Anweisung
in üblichen Programmiersprachen.
I
In Smalltalk ist durch Konvention festgelegt, dass globale
Variable mit einem Großbuchstaben beginnen müssen.
Steuerstrukturen in Smalltalk
1. Eine Reihe von Smalltalk-Ausdrücken kann mit eventuellen
formalen Parametern zu einem Block zusammengefasst
werden. Solch ein Block bildet eine unbenannte Funktion. Will
man den Block auswerten, so muss man ihm eine
value-Nachricht mit eventuellen Argumenten schicken.
2. Steuerbefehle sind nicht Teil der Sprache Smalltalk, sondern
können von den Benutzern selbst geschrieben werden. Sie
werden meist durch das Versenden von Nachrichten mit
Blockargumenten an boolesche Objekte realisiert.
3. Schleifen (Iterationen) werden über Iteratoren realisiert, die
den Schleifenrumpf als Blockargument übergeben bekommen.
Herunterladen