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.