Skript
Transcription
Skript
Karlheinz Hug Informatik 3 Skript Teil 1 Konzepte und Entwürfe objektorientierter Programmiersprachen Bachelor-Studiengang Medien- und Kommunikationsinformatik Fakultät Informatik Hochschule Reutlingen Reutlingen University 27. September 2012 ii Maskulin – Feminin Alle Berufs-, Rollen- und Personenbezeichnungen in diesem Skript und begleitenden Dokumenten, die als generische Maskulina wie Leser, Benutzer, Entwickler geschrieben sind, beziehen sich unabhängig von ihrem grammatischen Geschlecht stets auf Menschen beiderlei biologischen Geschlechts. Warenzeichen BlackBox und Component Pascal sind eingetragene Warenzeichen von Oberon microsystems. Design by Contract ist ein eingetragenes Warenzeichen von Eiffel Software. Eiffel ist ein Warenzeichen von Nonprofit International Consortium for Eiffel (NICE). Java ist ein eingetragenes Warenzeichen von Oracle Corporation. Visual C# ist ein eingetragenes Warenzeichen von Microsoft Corporation. Ada, BON, C, C++, Go, HTML, Modula, Oberon, OCL, Pascal, Prolog, Scala, UML, XML und andere Namen sind vielleicht eingetragene Warenzeichen. Erscheinen in diesem Dokument Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw., so berechtigt dies nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichenund Markenschutz-Gesetze als frei zu betrachten wären und daher von jedem benutzt werden dürften. Garantieverzichtserklärung Alle in diesem Skript und begleitenden Dokumenten enthaltenen Informationen wie Texte, Bilder, Programme und Verfahren wurden sorgfältig erstellt und geprüft. Da Fehler trotzdem nicht auszuschließen sind, ist der Inhalt des Skripts mit keinerlei Verpflichtung oder Garantie verbunden. Der Autor übernimmt weder eine juristische Verantwortung für eventuell verbliebene fehlerhafte Angaben und deren Folgen, noch irgendeine Haftung für Schäden, die in Zusammenhang mit der Verwendung dieses Dokuments, der darin dargestellten Methoden und Programme, oder Teilen davon entstehen. Missbrauchsverbot Die durch dieses Skript vermittelten Informationen wie Prinzipien, Konzepte, Methoden, Verfahren und Techniken dürfen nicht für militärische, völkerrechtswidrige, rassistische oder sonstige inhumane Zwecke missbraucht werden. Anschrift des Autors Prof. Dr. Karlheinz Hug Hochschule Reutlingen – Reutlingen University Fakultät Informatik Studiengang Medien- und Kommunikationsinformatik Alteburgstraße 150 72762 Reutlingen Bundesrepublik Deutschland – Germany Gebäude-Raum 9-126 Telefon +49/7121/271- 4013 E-Mail [email protected] [email protected] WWW http://www.mki.reutlingen-university.de Online-Dienst http://informatik.karlheinz-hug.de ftp://studinf.reutlingen-university.de/MKI/Hug © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Vorwort Aufgabe 1 Bild 1 meiner 1 (1) Sprache Leitlinie Programm 1bedeuten1die Grenzen Tabelle 1 DieBeispiel Grenzen meiner Welt. Ludwig Wittgenstein (1889 – 1951) Tractatus logico-philosophicus, 5.6 Voraussetzungen Dieses Skript und das Lehrmodul Informatik 3 setzen Grundkenntnisse der praktischen Informatik etwa im Umfang der zwei vierstündigen Vorlesungen Informatik 1 und 2 und in begleitenden Praktika erworbene Fähigkeiten im Programmieren mit einer objektorientierten Programmiersprache voraus. Gefordert sind zudem Grundkenntnisse der Mathematik, der Logik und der theoretischen Informatik etwa im Umfang der Lehrmodule Theoretische Grundlagen 1 und 2. Wer nur einen Hammer kennt, dem erscheint jedes Ding als Nagel „Für Informatikabsolventen genügt es, eine gängige Programmiersprache zu beherrschen.“ Einige Informatikstudiengänge folgen diesem engen Ein-Sprachen-Ansatz. Ist er einer akademischen Informatikausbildung didaktisch angemessen? Von den tausenden entwickelten Programmiersprachen werden viele noch benutzt, und es entstehen weiter neue, denn jeder Entwicklungsstand ist vorläufig. Ohne Programmiersprachen ist alles Programmieren nichts, aber Programmiersprachen sind nicht alles. Sie sind nur Mittel – wie ein Hammer zum Bearbeiten von Metallstücken. Doch kann, wer einen Hammer greift, schon ein brauchbares, kunstvolles Eisentor schmieden? Erfordert das nicht vielmehr handwerkliche und kreative Fähigkeiten? Was? Ein Informatikstudium, das zu einer 45jährigen Berufspraxis qualifizieren soll, muss vor allem Kenntnisse und Fähigkeiten mit langer Halbwertszeit vermitteln. So empfiehlt sich ein ganzheitlicher Ansatz, der sich auf Prinzipien, Konzepte, Methoden und Techniken der Softwareentwicklung konzentriert, denen Mittel und Werkzeuge, also auch Programmiersprachen, untergeordnet sind. Dieses Skript versucht eine Gratwanderung: Es stellt Konzepte und Entwürfe objektorientierter Programmiersprachen anhand wichtiger Algorithmen, Datenstrukturen, Entwurfsmuster und Programmiertechniken vor. Wozu Sprachkonzepte studieren? Konzepte von Programmiersprachen zu studieren ist nützlich, weil es diese Fähigkeiten verbessert [PrZ98] S. 22ff, [Seb06] S. 2ff: Ideen und Lösungen zu informatischen Problemen formulieren, die historische Entwicklung von Programmiersprachen verstehen, die Bedeutung von Sprachimplementationen erkennen, die aktuelle Programmiersprache effektiv und effizient benutzen, selbstständig schnell weitere Programmiersprachen lernen, für eine Anwendung die am besten geeignete Sprache wählen, neue Sprachen mit geforderten Eigenschaften entwerfen. Auch die letzte Fähigkeit ist nicht nur von wenigen Informatikern gefordert, denn letztlich definiert jede Benutzungsoberfläche, jede Schnittstelle eine neue Sprache. Wozu Sprachentwürfe studieren? Entwürfe verschiedener Programmiersprachen zu studieren ist insbesondere für Medien- und Kommunikationsinformatiker interessant, weil dies nicht nur technisch-pragmatische, sondern auch menschlich-pragmatische, gestalterische und ästhetische Aspekte umfasst. Wie es mehr oder minder benutzerfreundliche und ansprechende Webauftritte gibt, so gibt es mehr oder minder entwicklerfreundliche und elegante Programmiersprachen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 iii iv Programmierparadigma Vorwort sich die Informatik ständig neue Anwendungsbereiche erschließt, die neue Programmiersprachen erfordern. So hat etwa das Web zur Entwicklung von HTML, CSS, XML, Java, JavaScript, JSP, PHP, Curl, Ajax geführt. Sprachen mögen neu sein, umgesetzte Konzepte sind oft bekannt. viele Schwächen und Mängel etablierter Programmiersprachen in innovativen Sprachen schon überwunden sind. Allmählich setzen sich Verbesserungen auch in gängigen Sprachen durch, wie die Entwicklung C++ – Java – D – C# – Spec# – Scala – Go zeigt. Niemand kennt alle Programmiersprachen. Möglichst viele Details möglichst vieler Sprachen vermitteln zu wollen, wäre so unmöglich wie unsinnig. Sinnvoll ist dagegen, die Menge der Programmiersprachen nach verschiedenen Aspekten zu strukturieren und wiederkehrende Ideen heraus zu abstrahieren. Welche Programmierparadigmen und Konzepte soll dieses Skript behandeln, welche Entwürfe und Sprachen dazu auswählen? Bjarne Stroustrup rät in einem Interview in [Seb06] S. 507: „It is essential for anyone who wants to be considered a professional in the areas of software to know several languages and several programming paradigms. [...] Much of the inspiration to good programming comes from having learned and appreciated several programming styles and seen how they can be used in different languages. Furthermore, I consider programming of any nontrivial program a job for professionals with a solid and broad education, rather than for people with a hurried and narrow "training."“ Niklaus Wirth akzentuiert in einem Interview in [Seb06] S. 441: Statisch typisierte objektorientierte Mehrzwecksprache „It is important that CS [computer science, K.H.] students know the various programming paradigms: procedural, functional, logic, and object-oriented. But obviously the procedural style remains closest to the computer on which programs are interpreted ("run"). [...] The object-oriented style is based on the procedural style; it is a variant of it, not really different, even if procedures are now called "methods," and calling a procedure is termed "sending a message." I consider functional and logic programming "niche styles" much rather than procedural programming.“ Das Lehrmodul Informatik 3 setzt enge Zeitgrenzen. Deshalb konzentriert sich das Skript auf die für die Medien- und Kommunikationsinformatik praktisch relevante objektorientierte Ausprägung des imperativen Paradigmas. Andere Programmierparadigmen bleiben leider ebenso außer Betracht wie frühe imperative Sprachen, Skriptsprachen und aktuelle Sprachentwicklungen im Internet-Bereich – nur 1.2 S. 1-4 gibt einen Überblick. Das Skript beschränkt sich auf statisch typisierte objektorientierte Mehrzwecksprachen, die interessante Konzepte zur Entwicklung der Programmiersprachen beigetragen haben oder zurzeit in der industriellen Praxis verbreitet sind. Programmierpraxis Die ausgewählten Sprachen dienen im Informatik 3 Praktikum als Implementationssprachen, damit die Studierenden in diesen Sprachen programmieren lernen, die Basis ihrer praktischen Programmiererfahrung erweitern und so gut vorbereitet in die Praxisphase eintreten können. Wie? Der Ansatz dazu ist, von Kenntnissen der ersten Lehrsprache Component Pascal ausgehend Eiffel, C++, Java und C# zu erschließen. Das Skript fokussiert auf gemeinsame, wesentliche Sprachkonzepte (die meist von Component Pascal bekannt sind) und vergleicht die verschiedenen Entwürfe einzelner Sprachkonzepte. Auf dem Weg von Component Pascal zu rein objektorientiertem Programmieren ist Eiffel nützlich, weil diese rein objektorientierte Sprache theoretische Grundlagen abstrakter Datentypen und Klassen reflektiert, objektorientierte Konzepte klar umsetzt und mit der Idee der vertraglichen Spezifikation verbindet, und sich deshalb besonders gut für objektorientierte Softwareentwicklung eignet. Dagegen sind die Sprachentwürfe für C++, Java und C# eher pragmatisch vom Wunsch geprägt, sie evolutionär von der weit verbreiteten pro- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Vorwort v zeduralen Systemimplementationssprache C hin zu mehr Objektorientierung zu entwickeln, ohne sich zu weit von Traditionen der C-Welt zu entfernen. Deshalb heißen C++, Java und C# hier kurz C*-Sprachen. Dass sie in einer Entwicklungslinie liegen, begrenzt den Lernaufwand. Probleme lösen Sprachkonzepte und -konstrukte lernt man am besten an Beispielen. In der Literatur sind „Wegwerfbeispiele“ verbreitet, die keinen Nutzen haben außer dem, bestimmte Sprachkonstrukte vorzustellen. Dagegen bevorzugt dieses Skript Beispiele, die praktisch relevante Probleme durch Algorithmen, Datenstrukturen, Entwurfsmuster und Programmiertechniken aus dem Standardwissensschatz von Informatikern lösen. Zudem stellt es solche Beispiele nicht nur in wesentlichen Fragmenten, sondern in vollständigen ausführbaren Programmen vor. Damit lernen die Studenten nicht nur Sprachen, sondern wichtige Lösungen zu Programmierproblemen kennen. Vom Groben zum Feinen Der Ansatz des Problemlösens bis zum ausführbaren Programm bedingt, dass man sich früh auch mit Details der einzelnen Sprachen befassen muss. Trotzdem versucht das Skript, nicht den für Einführungen in das Programmieren im Grundstudium üblichen Weg von den kleinen Einheiten der Sprachen wie Zeichensätze, Literale, Variablen, Ausdrücke, Anweisungen zu den großen Einheiten wie Module, Klassen, Pakete zu gehen, sondern den umgekehrten Weg einzuschlagen: von softwaretechnisch relevanten Strukturierungskonzepten zu nachrangigen Details. Vergleichen Bekannten Eigenschaften von Component Pascal stellt das Skript zunächst entsprechende Eigenschaften von Eiffel und C* gegenüber. Der Leser rekapituliert damit wesentliche Programmierkonzepte, die Component Pascal unterstützt, und erfährt beispielhaft, wie andere Sprachen sie realisieren. Danach befasst sich das Skript mit speziellen Merkmalen der einzelnen Sprachen, die Component Pascal nicht kennt. Nicht beabsichtigt ist, die Sprachen vollständig darzustellen, zu vergleichen oder zu bewerten. Versucht man, Programmiersprachen Merkmal für Merkmal zu vergleichen, so läuft man Gefahr, Sprachelemente aus ihrem Kontext zu isolieren und ihr Zusammenwirken mit anderen Elementen zu ignorieren. Um einer Programmiersprache gerecht zu werden, muss man ihre Eigenschaften in ihrem Zusammenhang betrachten. Abstrahieren Trotz dieser Vorbehalte ist der „vergleichende“ Ansatz als Einstieg in weitere Programmiersprachen gewählt, weil sich damit auch das Allgemeine an konkreten Realisierungen programmiersprachlicher Konzepte verdeutlichen und abstrahieren lässt. Beispielprogramme und -fragmente sollen die Gemeinsamkeiten der Sprachen herausstellen, nicht unbedingt alle ihre speziellen Merkmale einsetzen. Mit dieser Sichtweise sollte das selbstständige Lernen weiterer imperativer Programmiersprachen wenig Schwierigkeiten bereiten. Die Fähigkeit dazu ist z.B. wegen der weiten Verbreitung vieler Skriptsprachen vorteilhaft. Zudem wird sich zeigen, dass die Kenntnis effektiver Entwurfsmuster und effizienter Algorithmen wichtiger ist und sich schwerer selbstständig aneignen lässt als etwa die Kenntnis des Schleifenkonstrukts in dieser oder der Klassendefinition in jener Sprache. Struktur des Skripts Kapitel 1 führt in verschiedene Aspekte von Programmiersprachen ein. Die nächsten drei Kapitel folgen dem erwähnten Probleme-lösen-Ansatz und bringen Programmbeispiele in den ausgewählten Sprachen Component Pascal, Eiffel, C++, Java und C#, ausgehend von ganz einfachen in Kapitel 2 bis zu solchen in den Kapiteln 3 und 4, die Polymorphie, Generizität und Entwurfsmuster demonstrieren. Danach folgen die Kapitel 5 bis 9 dem erwähnten Top-Down-Ansatz. Literatur Das Literaturverzeichnis enthält vor allem Verweise auf Lehrbücher, gegliedert in sprachübergreifende Literatur und Literatur zu einzelnen Sprachen. Literaturhinweise sind im Text durch Kurzbelege des Literaturverzeichnisses wie [ECMA367] angegeben. Nur einmalig referenzierte Web-Adressen erscheinen dagegen eher in Fußnoten. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 vi Vorwort Das Konzept dieses Skripts orientiert sich an der Art von Lehrbüchern, die im Literaturverzeichnis unter der Marginalie Programmiersprachen S. A-3 zu finden sind. Quellen für vorgestellte Sprachelemente sind vorzüglich originale Sprachbeschreibungen. Oberon-2 ist in [ReW94], [Mös94] beschrieben, Component Pascal in [Hug01], [War02]. Die Quellen für Eiffel sind der ECMA-Standard [ECMA367] und der NICEStandard [ER06], [Mey95], [Mey92]. Bei C++ geht das Skript nicht auf Dialekte ein, sondern orientiert sich am ANSI/ISO-Standard [C++11], [C++98], [Str97], [ElS90]. Referenzen für Java sind [GJSBB11], [GJSB05], [GJSB00], [GJS96], für C# [ECMA334], [C#PR12], [WiH04], [HWG04]. Das Skript ersetzt keine Einführungen in das Programmieren mit Eiffel oder C*, dazu stellt es die Sprachelemente zu lückenhaft dar. Zu Eiffel gibt es eine Reihe guter Einführungen. Die Literatur zu den C*-Sprachen ist unüberschaubar groß, sodass dieses Rad nicht neu zu erfinden ist. Der Leser sei zur begleitenden Lektüre auf Lehrbücher verwiesen sowie auf die vielen Tutorien, die im Web leicht zu finden sind. Dokumentationssprache Englisch Der Text versucht, der neuen deutschen Rechtschreibung zu folgen. Modelle, Spezifikationen und Programme sind englisch formuliert. Die Gründe dafür sind vielschichtig: Programmiersprachen orientieren sich am Englischen. Englisch ist international verbreitet, in vielen Firmen Dokumentationssprache und wird durch die Globalisierung in der Praxis von Softwareentwicklern immer wichtiger. Wiederverwendbare Softwarekomponenten müssen englisch dokumentiert sein, um einen Markt zu finden. Außerdem sind englische Wörter oft kürzer als entsprechende deutsche. Darstellung des Texts Bezeichnungen besonders wichtiger Begriffe und neu eingeführte Bezeichnungen sind bei ihrem ersten Auftreten oder ihrer Definition durch Fettdruck hervorgehoben. Andere wichtige, aber anderswo definierte Bezeichnungen, in der Literatur verwendete Synonyme und englische Bezeichnungen erscheinen kursiv. Zwecks leichten Erkennens sind auch Namen von Programmiersprachen, Werkzeugen und anderen Produkten kursiv geschrieben. Notation für Programmtext Programmtexte verwenden die Schriftarten, Formatierungen, Namenskonventionen und Einrückungsregeln der jeweiligen offiziellen Beschreibung der Programmiersprache. Bei Java und C# sind immer noch Festbreitschriften üblich, während Component Pascal und Eiffel seit langem besser lesbare Proportionalschriften mit Kursiv- und Fettstil benutzen. Für C++ ist Festbreitschrift verbreitet, aber Stroustrup verwendet seit 1997 kursive Proportionalschrift [Str97]. Einzelne Zeilen sind mit speziellen Symbolen markiert: weist auf etwas Wichtiges oder nachfolgend Erläutertes hin, ärgert sich über eine mangelhafte Programmstelle oder etwas Nachteiliges, ☺ erfreut sich an der korrigierten Programmstelle oder etwas Vorteilhaftem, warnt vor einer fehlerhaften Programmstelle oder einem gefährlichen Konstrukt. Nun wünsche ich dem Leser viel Spaß beim Lesen. Hinweise auf Fehler, Kritik und Zustimmung nehme ich gerne entgegen. Reutlingen, den 9. März 2012 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Karlheinz Hug 27.9.12 Inhaltsverzeichnis Vorwort ....................................................................................................................... iii Bilderverzeichnis ........................................................................................................xv Tabellenverzeichnis ................................................................................................. xvii Programmverzeichnis .............................................................................................. xix Beispielverzeichnis ...................................................................................................xxv Leitlinienverzeichnis ............................................................................................. xxvii Aufgabenverzeichnis .............................................................................................. xxix 1 Einführung 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 Klassifikation von Programmiersprachen ......................................................1 Programmierparadigmen ................................................................................4 1.2.1 Imperatives Programmierparadigma ..................................................5 1.2.2 Funktionales Programmierparadigma ................................................9 1.2.3 Logikbasiertes Programmierparadigma ...........................................12 1.2.4 Regelbasiertes Programmierparadigma ............................................12 1.2.5 Fazit ..................................................................................................13 Abstraktionsebenen in imperativen Sprachen ..............................................14 Soll und Ist von Programmiersprachen ........................................................16 1.4.1 Beschreibung von Programmiersprachen .........................................16 1.4.1.1 Pragmatik .............................................................................16 1.4.1.2 Semantik ...............................................................................17 1.4.1.3 Syntax ...................................................................................17 1.4.2 Implementation von Programmiersprachen .....................................18 1.4.2.1 Darstellung in der Zielsprache .............................................18 1.4.2.2 Werkzeuge ............................................................................19 1.4.2.3 Implementationskonzepte .....................................................19 1.4.3 Vielfalt und Einheit von Programmiersprachen ...............................20 1.4.4 Umgebungen von Programmiersprachen .........................................20 Einflüsse auf und von Programmiersprachen ...............................................20 1.5.1 Einflüsse auf Programmiersprachen .................................................20 1.5.2 Einflüsse von Programmiersprachen ................................................20 Entwicklung imperativer Sprachen ..............................................................21 Merkmale von Programmiersprachen ..........................................................23 1.7.1 Nichttechnische Merkmale ...............................................................23 1.7.2 Technische Merkmale ......................................................................23 1.7.3 Qualitätsmerkmale von Sprachbeschreibungen ...............................23 1.7.4 Qualitätsmerkmale von Sprachimplementationen ...........................24 Überblick über die Auswahlsprachen ...........................................................25 1.8.1 Ziele der Sprachentwürfe .................................................................25 1.8.2 Quantitative Eigenschaften ..............................................................27 1.8.3 Grundstrukturen von Programmen ...................................................28 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 vii viii Inhaltsverzeichnis 1.8.4 1.8.5 1.8.6 1.8.7 2 Einmal quer durch die Sprachen 2.1 2.2 2.3 2.4 2.5 3 Unterstützte Programmierstile, Abstraktionsebenen, Sprachkonzepte ...........................................................................................................29 Sprachdefinitionen und Syntaxbeschreibungen ...............................32 Sprachimplementationen ..................................................................32 Zielsprachen und Übersetzungskonzepte .........................................33 Hello-World-Programme ................................................................................1 2.1.1 Component Pascal ..............................................................................1 2.1.2 Eiffel ...................................................................................................2 2.1.3 C++ .....................................................................................................3 2.1.4 Java .....................................................................................................8 2.1.5 C# .....................................................................................................10 2.1.6 Weitere Sprachen ..............................................................................11 2.1.7 Fazit ..................................................................................................12 Argumentübernahme von der Kommandozeile ............................................13 2.2.1 Component Pascal ............................................................................13 2.2.1.1 Argumentübernahme ............................................................13 2.2.1.2 Konversion ...........................................................................14 2.2.2 Eiffel .................................................................................................14 2.2.2.1 Argumentübernahme ............................................................14 2.2.2.2 Konversion ...........................................................................16 2.2.2.3 Entfernte Argumentübernahme ............................................17 2.2.3 C++ ...................................................................................................19 2.2.3.1 Argumentübernahme ............................................................19 2.2.3.2 Konversion ...........................................................................21 2.2.4 Java ...................................................................................................21 2.2.4.1 Argumentübernahme ............................................................21 2.2.4.2 Konversion ...........................................................................22 2.2.5 C# .....................................................................................................23 2.2.5.1 Argumentübernahme ............................................................23 2.2.5.2 Konversion ...........................................................................24 2.2.6 Fazit ..................................................................................................25 Uhrzeit ..........................................................................................................26 2.3.1 Component Pascal ............................................................................27 2.3.2 Eiffel .................................................................................................29 2.3.3 C++ ...................................................................................................31 2.3.4 Java ...................................................................................................34 2.3.5 C# .....................................................................................................35 2.3.6 Fazit ..................................................................................................37 Aufgaben ......................................................................................................38 Lösungen ......................................................................................................41 Funktional, rekursiv, iterativ, objektorientiert, abstrakt 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional .......................1 3.1.1 Funktionale Sprachen .........................................................................2 3.1.2 Component Pascal ..............................................................................4 3.1.3 Eiffel ...................................................................................................8 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Inhaltsverzeichnis ix 3.2 3.3 27.9.12 3.1.4 C++ ...................................................................................................11 3.1.5 Java ...................................................................................................12 3.1.6 C# .....................................................................................................13 3.1.7 Fazit ..................................................................................................14 3.1.8 Aufgaben ..........................................................................................15 Größter gemeinsamer Teiler – objektorientiert .............................................17 3.2.1 Entwurf .............................................................................................18 3.2.2 Eiffel .................................................................................................19 3.2.2.1 Schnittstellenklasse ..............................................................19 3.2.2.2 Implementationsklassen .......................................................20 3.2.2.3 Kundenklasse .......................................................................22 3.2.3 C++ ...................................................................................................24 3.2.3.1 Schnittstellenklasse ..............................................................24 3.2.3.2 Implementationsklassen .......................................................24 3.2.3.3 Kundenfunktion ....................................................................27 3.2.4 Java ...................................................................................................28 3.2.4.1 Schnittstellenklasse ..............................................................29 3.2.4.2 Implementationsklassen .......................................................29 3.2.4.3 Kundenklasse .......................................................................30 3.2.5 C# .....................................................................................................31 3.2.5.1 Schnittstellenklassen ............................................................31 3.2.5.2 Implementationsklassen .......................................................32 3.2.5.3 Kundenklasse .......................................................................33 3.2.6 Fazit ..................................................................................................34 3.2.7 Aufgaben ..........................................................................................34 Abstraktion mit Konzeptklassen und Schablonenmethoden ........................35 3.3.1 Teile und herrsche – funktional ........................................................36 3.3.2 Objektorientierter Entwurf der abstrakten Problemlösung nur mit Abfragen ...........................................................................................38 3.3.3 Eiffel – Konzeptklassen ....................................................................38 3.3.4 Objektorientierter Entwurf der konkreten Problemlösung ...............42 3.3.5 Eiffel .................................................................................................42 3.3.5.1 Schnittstellenklassen ............................................................42 3.3.5.2 Implementationsklassen .......................................................44 3.3.6 Objektorientierter Entwurf mit Abfragen und Aktionen ..................46 3.3.7 Eiffel .................................................................................................47 3.3.7.1 Konzeptklasse .......................................................................47 3.3.7.2 Implementationsklassen .......................................................48 3.3.7.3 Kundenklasse .......................................................................49 3.3.8 C++ ...................................................................................................51 3.3.8.1 Konzeptklassen .....................................................................51 3.3.8.2 Implementationsklassen .......................................................52 3.3.8.3 Kundenfunktion ....................................................................55 3.3.9 Java ...................................................................................................55 3.3.9.1 Konzeptklassen .....................................................................55 3.3.9.2 Implementationsklassen .......................................................56 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 x Inhaltsverzeichnis 3.3.9.3 Kundenklasse .......................................................................57 3.3.10 C# .....................................................................................................57 3.3.10.1 Konzeptklassen ...................................................................57 3.3.10.2 Implementationsklassen .....................................................58 3.3.10.3 Kundenklasse .....................................................................59 3.3.11 Fazit ..................................................................................................59 3.3.12 Aufgaben ..........................................................................................60 4 Reihungen und Abstraktionen 4.1 4.2 4.3 4.4 Binäres Suchen in einer sortierten Reihung ...................................................1 4.1.1 Trennen wiederverwendbarer Komponenten von Anwendungen ......2 4.1.2 Routinen schachteln oder nicht? ........................................................2 4.1.3 Component Pascal ..............................................................................2 4.1.4 Eiffel ...................................................................................................6 4.1.4.1 Werkzeugkasten ......................................................................6 4.1.4.2 Testtreiber ...............................................................................8 4.1.5 C++ ...................................................................................................10 4.1.5.1 Werkzeugkasten: Namensraum und generische Funktionen 10 4.1.5.2 Werkzeugkasten: Generische Klasse und Klassenfunktionen ...........................................................................................14 4.1.5.3 Testtreiber .............................................................................16 4.1.6 Java ...................................................................................................17 4.1.6.1 Werkzeugkasten ....................................................................17 4.1.6.2 Testtreiber .............................................................................19 4.1.7 C# .....................................................................................................20 4.1.8 Fazit ..................................................................................................21 Sortieren einer Reihung mit Quicksort .........................................................22 4.2.1 Component Pascal ............................................................................23 4.2.2 Eiffel .................................................................................................24 4.2.3 C++ ...................................................................................................24 4.2.4 Java ...................................................................................................25 4.2.5 C# .....................................................................................................25 4.2.6 Fazit ..................................................................................................25 Indexbereich mit maximaler Summe ............................................................25 4.3.1 Ersetzen von Ausgabeparametern ....................................................25 4.3.2 Component Pascal ............................................................................26 4.3.3 Eiffel .................................................................................................27 4.3.4 C++ ...................................................................................................29 4.3.5 Java ...................................................................................................31 4.3.6 C# .....................................................................................................31 4.3.7 Fazit ..................................................................................................31 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen .....32 4.4.1 Eiffel .................................................................................................32 4.4.1.1 Initialisierer ..........................................................................32 4.4.1.2 Testtreiber .............................................................................37 4.4.2 C++ ...................................................................................................42 4.4.2.1 Initialisierer ..........................................................................43 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Inhaltsverzeichnis xi 4.4.3 4.4.4 4.4.5 5 Programmeinheiten und -strukturen 5.1 5.2 5.3 5.4 6 Kompilationsarten und -einheiten ..................................................................1 5.1.1 Component Pascal ..............................................................................2 5.1.2 Eiffel ...................................................................................................3 5.1.3 C++ .....................................................................................................3 5.1.4 Java .....................................................................................................4 5.1.5 C# .......................................................................................................4 Zusammenfassung und Kapselung von Programmteilen ...............................5 5.2.1 Component Pascal ..............................................................................5 5.2.2 Eiffel ...................................................................................................6 5.2.3 C++ .....................................................................................................6 5.2.4 Java .....................................................................................................8 5.2.5 C# .......................................................................................................8 Geheimnisprinzip, Schutz und Zugriffsrechte ................................................9 5.3.1 Component Pascal ..............................................................................9 5.3.2 Eiffel .................................................................................................10 5.3.3 C++ ...................................................................................................10 5.3.4 Java ...................................................................................................11 5.3.5 C# .....................................................................................................11 Module und Datenabstraktion ......................................................................14 5.4.1 Component Pascal ............................................................................14 5.4.2 Eiffel .................................................................................................15 5.4.3 C++ ...................................................................................................15 5.4.3.1 Prämodule .............................................................................15 5.4.3.2 Namensräume .......................................................................15 5.4.3.3 Klassenmethoden .................................................................17 5.4.4 Java, C# ............................................................................................17 Klassen und objektorientierte Konzepte 6.1 6.2 6.3 6.4 6.5 27.9.12 4.4.2.2 Testtreiber .............................................................................44 Java ...................................................................................................46 C# .....................................................................................................46 Fazit ..................................................................................................46 Abstrakte Datentypen .....................................................................................1 Klassen ............................................................................................................1 6.2.1 Bestandteile von Klassen ...................................................................4 6.2.2 Verhältnis zu Kompilationseinheiten .................................................4 Objekte ............................................................................................................5 6.3.1 Beispiel für Wertobjekt ......................................................................5 6.3.2 Beispiel für Referenzobjekt ................................................................6 6.3.3 Zugriffe auf Objektmerkmale .............................................................6 Klassendaten und -operationen .......................................................................7 6.4.1 Klassendaten .......................................................................................7 6.4.2 Klassenoperationen ............................................................................8 Abstrakte Klassen ...........................................................................................9 6.5.1 Gemeinsames .....................................................................................9 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 xii Inhaltsverzeichnis 6.5.2 Unterschiedliches .............................................................................10 6.6 Vererbung ......................................................................................................11 6.7 Anpassung geerbter Merkmale .....................................................................12 6.8 Polymorphie und dynamisches Binden ........................................................13 6.8.1 Gemeinsames ...................................................................................13 6.8.2 Historisches ......................................................................................14 6.8.3 Component Pascal ............................................................................14 6.8.4 Eiffel .................................................................................................14 6.8.5 C++ ...................................................................................................15 6.8.6 Java ...................................................................................................17 6.8.7 C# .....................................................................................................18 6.9 Redefinitionsregeln .......................................................................................19 6.10 Zugriffe auf Oberklassenversionen geerbter Operationen ............................19 6.11 Schachtelung .................................................................................................20 6.12 Speicherverwaltung ......................................................................................21 7 Typen und Datenkonzepte 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10 7.11 7.12 7.13 7.14 7.15 7.16 7.17 7.18 7.19 7.20 7.21 8 Gemeinsamkeiten ...........................................................................................1 Unterschiede ...................................................................................................1 Typsystem .......................................................................................................1 Sprachdefinierte Typen mit Operatoren ..........................................................2 Programmiererdefinierte Typen ......................................................................6 7.5.1 Operatoren ..........................................................................................7 Typdeklarationen, Typdefinitionen .................................................................7 Typgleichheit, Typäquivalenz .........................................................................8 Wertprüfungen zur Laufzeit ............................................................................8 Implizite Typanpassung ..................................................................................8 Explizite Typanpassung, Typkonversion ........................................................9 Statischer und dynamischer Typ .....................................................................9 Typinformationen zur Laufzeit .....................................................................10 Typprüfungen zur Laufzeit ...........................................................................11 Typkonversionen zur Laufzeit ......................................................................11 Zeiger ............................................................................................................12 Reihungen .....................................................................................................12 Zeichenketten ................................................................................................14 Verbunde .......................................................................................................14 Mengen .........................................................................................................15 Einfache Typen .............................................................................................15 7.20.1 Zeichen und Zahlen ..........................................................................15 7.20.2 Boolesche Daten ...............................................................................16 7.20.3 Aufzählungen ...................................................................................16 Konstanten ....................................................................................................17 Ablaufsteuerung und algorithmische Konzepte 8.1 Routinen ..........................................................................................................1 8.1.1 Prozeduren ..........................................................................................1 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Inhaltsverzeichnis xiii 8.1.2 8.1.3 8.1.4 8.1.5 8.1.6 8.2 8.3 8.4 9 Funktionen ..........................................................................................2 Parameter- und Ergebnisübergabe ......................................................3 Eigenschaften von Routinen ...............................................................5 Routinentypen und -variablen ............................................................6 Was entspricht den vordeklarierten Component-Pascal-Prozeduren? .............................................................................................................7 8.1.7 Was entspricht den vordeklarierten Component-Pascal-Funktionen? .............................................................................................................9 Zusicherungen ..............................................................................................12 Anweisungen ................................................................................................14 8.3.1 Übersicht ..........................................................................................14 8.3.2 Syntaktischer Zucker ........................................................................15 8.3.3 Semantisches Salz ............................................................................15 8.3.4 Zuweisungen ....................................................................................16 8.3.5 Auswahlanweisungen .......................................................................16 8.3.5.1 Alternative Bedingungen ......................................................16 8.3.5.2 Alternative Werte .................................................................17 8.3.6 Wiederholungsanweisungen .............................................................18 Testhilfen ......................................................................................................19 Syntaktische und lexikalische Aspekte 9.1 9.2 9.3 9.4 Kommentare ...................................................................................................1 Zeichencodes ..................................................................................................1 Bezeichner ......................................................................................................1 Wortsymbole ...................................................................................................2 A Literaturverzeichnis 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 xiv © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Inhaltsverzeichnis 27.9.12 Bilderverzeichnis 1 Einführung Bild 1.1 Entwickler – Programm – Rechner ............................................................1 Bild 1.2 Hierarchie virtueller Rechner .....................................................................4 Bild 1.3 Zustandsübergangsdiagramm ....................................................................6 Bild 1.4 Struktogramm ............................................................................................6 Bild 1.5 Prozeduren mit Benutzung und Schachtelung ...........................................7 Bild 1.6 Module mit Benutzung und Schachtelung .................................................7 Bild 1.7 Abstrakter Datentyp mit Exemplaren ........................................................8 Bild 1.8 Klassen mit Benutzung und Vererbung .....................................................8 Bild 1.9 Komponenten .............................................................................................9 Bild 1.10 Generische Einheit mit Konkretisierungen ..............................................9 Bild 1.11 Aspekte von Programmiersprachen ......................................................16 Bild 1.12 Pragmatik ...............................................................................................17 Bild 1.13 Entwicklungslinien imperativer Programmiersprachen ........................22 2 Einmal quer durch die Sprachen Bild 2.1 Inkludierungsbeziehungen zu Programm 2.7 ...........................................6 Bild 2.2 Benutzen und beerben ..............................................................................17 Bild 2.3 Kunden-Lieferanten-Kette .......................................................................18 Bild 2.4 Feigenbaum-Diagramme ........................................................................40 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Bild 3.1 Kunde mit Schnittstellenklasse und Implementationsklassen .................18 Bild 3.2 Abstraktes Problem mit abstrakter Lösung ..............................................35 Bild 3.3 Konzeptklassen für rekursiv teilbares Problem nur mit Abfragen .........38 Bild 3.4 Konzept-, Schnittstellen- und Implementationsklassen für ggT-Problem nur mit Abfragen ....................................................................................................42 Bild 3.5 Konzept- und Implementationsklassen für ggT-Problem mit Abfragen und Aktionen .................................................................................................................47 4 Reihungen und Abstraktionen Bild 4.1 Modulares Trennen verschiedener Aspekte ...............................................2 Bild 4.2 Klassendiagramm zu Werkzeugkastenklassen .........................................28 Bild 4.3 Klassendiagramm zur Initialisierung von Reihungen ..............................32 Bild 4.4 Klassendiagramm zur Testtreibern .........................................................37 5 Programmeinheiten und -strukturen 6 Klassen und objektorientierte Konzepte 7 Typen und Datenkonzepte 8 Ablaufsteuerung und algorithmische Konzepte 9 Syntaktische und lexikalische Aspekte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xv xvi © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Bilderverzeichnis 27.9.12 Tabellenverzeichnis 1 2 3 4 5 6 7 8 9 Einführung Tabelle 1.1 Funktional: Fläche eines Rechtecks ...................................................11 Tabelle 1.2 Klassifikation der Auswahlsprachen .................................................25 Tabelle 1.3 Quantitative Eigenschaften ................................................................27 Tabelle 1.4 Minimale Programme ........................................................................29 Tabelle 1.5 Unterstützte Programmierstile ...........................................................30 Tabelle 1.6 Erreichte Abstraktionsebenen ............................................................31 Tabelle 1.7 Unterscheidende Sprachmerkmale ....................................................31 Einmal quer durch die Sprachen Funktional, rekursiv, iterativ, objektorientiert, abstrakt Reihungen und Abstraktionen Tabelle 4.1 Ersetzen von Parametern durch globale Variablen .............................26 Programmeinheiten und -strukturen Tabelle 5.1 Physische Einheiten .............................................................................2 Tabelle 5.2 Kapselung kleiner und großer Programmteile .....................................5 Tabelle 5.3 Schutz, Rechte und Verbote .................................................................9 Tabelle 5.4 Schutz und Zugriffsrechte bei Klassen ..............................................13 Klassen und objektorientierte Konzepte Tabelle 6.1 Bezeichnungsweisen ............................................................................3 Tabelle 6.2 Eigenschaften von Klassen ..................................................................4 Tabelle 6.3 Wert- und Referenzsemantik ...............................................................5 Tabelle 6.4 Zugriffe auf Objektmerkmale ..............................................................7 Tabelle 6.5 Vererbung ...........................................................................................11 Tabelle 6.6 Schachtelung ......................................................................................21 Typen und Datenkonzepte Tabelle 7.1 Vordefinierte Typen .............................................................................2 Tabelle 7.2 Programmiererdefinierte Typen ...........................................................6 Ablaufsteuerung und algorithmische Konzepte Tabelle 8.1 Routinen ................................................................................................1 Tabelle 8.2 Parameter- und Ergebnisübergabe .......................................................3 Tabelle 8.3 Eigenschaften von Routinen ................................................................5 Tabelle 8.4 Was entspricht den vordeklarierten Component-Pascal-Prozeduren? .8 Tabelle 8.5 Was entspricht den vordeklarierten Component-Pascal-Funktionen? .9 Tabelle 8.6 Anweisungen .....................................................................................14 Syntaktische und lexikalische Aspekte A Literaturverzeichnis © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xvii xviii © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Tabellenverzeichnis 27.9.12 Programmverzeichnis 1 2 Einführung Einmal quer durch die Sprachen Programm 2.1 Component Pascal: Hello World ....................................................1 Programm 2.2 Eiffel: Hello World 1 ......................................................................2 Programm 2.3 Eiffel: Hello World 2 ......................................................................2 Programm 2.4 C++: Hello World 1 ........................................................................3 Programm 2.5 C++: Hello World 2 ........................................................................4 Programm 2.6 C++: Hello World 3 mit Klasse ......................................................6 Programm 2.7 C++: Hello World 4 mit wiederverwendbarer Klasse ....................7 Programm 2.8 Java: Hello World 1 ........................................................................8 Programm 2.9 Java: Hello World 2 ........................................................................9 Programm 2.10 Java: Hello World 3 ....................................................................10 Programm 2.11 HTML: Applet-Aufruf HelloWorld3 ..........................................10 Programm 2.12 C#: Hello World 1 ......................................................................10 Programm 2.13 C#: Hello World 2 ......................................................................11 Programm 2.14 D: Hello World ...........................................................................11 Programm 2.15 Scala: Hello World 1 ..................................................................11 Programm 2.16 Scala: Hello World 2 ..................................................................11 Programm 2.17 Scala: Hello World 3 ..................................................................12 Programm 2.18 Go: Hello World .........................................................................12 Programm 2.19 Component Pascal: Parameterübernahme ..................................13 Programm 2.20 Eiffel: Argumentübernahme an der Wurzel ................................15 Programm 2.21 Eiffel: Argumentübernahme mit Mix-In ................................17 Programm 2.22 Eiffel: Argumentübernahme irgendwo ohne Mix-In...................18 Programm 2.23 Eiffel: Wurzelklasse zur Argumentausgabe ................................19 Programm 2.24 C++: Argumentübernahme .........................................................20 Programm 2.25 Java: Argumentübernahme 1 ......................................................22 Programm 2.26 Java: Argumentübernahme 2 ......................................................22 Programm 2.27 C#: Argumentübernahme 1 ........................................................23 Programm 2.28 C#: Argumentübernahme 2 ........................................................24 Programm 2.29 Component Pascal: Uhrzeit mit Empfänger-Referenzen ...........27 Programm 2.30 Component Pascal: Uhrzeit mit Empfänger-Zeigern .................28 Programm 2.31 Eiffel: Uhrzeit nicht expandiert ...................................................29 Programm 2.32 Eiffel: Uhrzeit expandiert ...........................................................31 Programm 2.33 Eiffel: Uhrzeit als expandierter Nachfolger von Programm 2.31 .................................................................................................................................31 Programm 2.34 Eiffel: Uhrzeit als nicht expandierter Nachfolger von Programm 2.32 .........................................................................................................................31 Programm 2.35 C++: Uhrzeit, Schnittstelle .........................................................32 Programm 2.36 C++: Uhrzeit, Implementation ....................................................33 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xix xx Programmverzeichnis 3 Programm 2.37 Java: Uhrzeit ...............................................................................35 Programm 2.38 C#: Uhrzeit als Referenztyp .......................................................36 Programm 2.39 C#: Uhrzeit als Werttyp ..............................................................37 Programm 2.40 C++: Schwache Schnittstelle und Implementation ............38 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Programm 3.1 Funktionaler Pseudocode: Größter gemeinsamer Teiler ................2 Programm 3.2 Scheme: Größter gemeinsamer Teiler ............................................3 Programm 3.3 ML: Größter gemeinsamer Teiler 1 ................................................3 Programm 3.4 ML: Größter gemeinsamer Teiler 2 ................................................3 Programm 3.5 Miranda: Größter gemeinsamer Teiler 1 ........................................3 Programm 3.6 Miranda: Größter gemeinsamer Teiler 2 ........................................3 Programm 3.7 Prolog: Größter gemeinsamer Teiler ..............................................4 Programm 3.8 Component Pascal: Größter gemeinsamer Teiler, rekursiv ............4 Programm 3.9 Component Pascal: Größter gemeinsamer Teiler, rekursiv ........4 Programm 3.10 Component Pascal: Größter gemeinsamer Teiler, umgeformt .....6 Programm 3.11 Component Pascal: Größter gemeinsamer Teiler, vereinfacht .....6 Programm 3.12 Component Pascal: Größter gemeinsamer Teiler, iterativ ............7 Programm 3.13 Python: Größter gemeinsamer Teiler mit kollektiver Zuweisung 7 Programm 3.14 Component Pascal: Rechner für größten gemeinsamen Teiler ....8 Programm 3.15 Eiffel: Größter gemeinsamer Teiler, spezifiziert ..........................8 Programm 3.16 Eiffel: Größter gemeinsamer Teiler, rekursiv ...............................8 Programm 3.17 Eiffel: Größter gemeinsamer Teiler, iterativ..................................9 Programm 3.18 Eiffel: Rechner für größten gemeinsamen Teiler .......................10 Programm 3.19 C++: Größter gemeinsamer Teiler, rekursiv ...............................11 Programm 3.20 C++: Größter gemeinsamer Teiler, fast funktional .....................11 Programm 3.21 C++: Größter gemeinsamer Teiler, iterativ ................................11 Programm 3.22 C++: Rechner für größten gemeinsamen Teiler .........................12 Programm 3.23 Java: Rechner für größten gemeinsamen Teiler .........................12 Programm 3.24 Java: Ausnahmen bei Fehleingaben ....................................13 Programm 3.25 C#: Rechner für größten gemeinsamen Teiler ............................14 Programm 3.26 C++: ggT-Funktion 1 ..................................................................15 Programm 3.27 C++: ggT-Funktion 2 ..................................................................15 Programm 3.28 C++: ggT-Funktion 3 ..................................................................15 Programm 3.29 C++: ggT-Funktion 4 ..................................................................15 Programm 3.30 C++: ggT-Funktion 5 ..................................................................16 Programm 3.31 C++: Beispiel von einer IBM-Webseite .................................17 Programm 3.32 C++: Beispiel aus der Prüfung Informatik 3 WS 2006/07 .....17 Programm 3.33 Eiffel: ggT, objektorientiert abstrakt ..........................................19 Programm 3.34 Eiffel: ggT, oo-endrekursiv mit Referenzsemantik ....................20 Programm 3.35 Eiffel: ggT, oo-iterativ mit Referenzsemantik ............................21 Programm 3.36 Eiffel: ggT-Rechner, objektorientiert ..........................................22 Programm 3.37 C++: ggT, objektorientiert abstrakt, Schnittstelle .......................24 Programm 3.38 C++: ggT, objektorientiert abstrakt, partielle Implementation ...24 Programm 3.39 C++: ggT, oo-endrekursiv, Schnittstelle .....................................25 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Programmverzeichnis xxi Programm 3.40 C++: ggT, oo-endrekursiv, Implementation mit Wertsemantik ..25 Programm 3.41 C++: ggT, oo-endrekursiv, Implementation mit Zeigersemantik .................................................................................................................................26 Programm 3.42 C++: ggT, oo-iterativ, Schnittstelle ............................................27 Programm 3.43 C++: ggT, oo-iterativ, Implementation .......................................27 Programm 3.44 C++: ggT-Rechner, objektorientiert ............................................28 Programm 3.45 Java: ggT, objektorientiert abstrakt ............................................29 Programm 3.46 Java: ggT, oo-endrekursiv ..........................................................29 Programm 3.47 Java: ggT, oo-iterativ ..................................................................30 Programm 3.48 Java: ggT-Rechner, objektorientiert ...........................................30 Programm 3.49 C#: ggT, objektorientiert abstrakt, Referenztyp ..........................31 Programm 3.50 C#: ggT, objektorientiert abstrakt, Werttyp ................................32 Programm 3.51 C#: ggT, oo-endrekursiv, Referenztyp.........................................32 Programm 3.52 C#: ggT, oo-iterativ, Werttyp ......................................................32 Programm 3.53 C#: ggT-Rechner, objektorientiert ..............................................33 Programm 3.54 Pseudocode: Problemlöser .........................................................36 Programm 3.55 Funktionaler Pseudocode: Endrekursiv gelöstes Problem .........37 Programm 3.56 Funktionaler Pseudocode: ggT als Konkretisierung ..................37 Programm 3.57 Eiffel: Generische Konzeptklasse für Problem ..........................38 Programm 3.58 Eiffel: Generische Konzeptklasse für teilbares Problem mit Abfragen ................................................................................................................39 Programm 3.59 Eiffel: Generische Konzeptklasse für endrekursiv gelöstes Problem ..................................................................................................................40 Programm 3.60 Eiffel: Schablonenmethode zu endrekursiv gelöstem Problem, umformungsbereit ..................................................................................................40 Programm 3.61 Pseudo-Eiffel: Schablonenmethode zu endrekursiv gelöstem Problem, umgeformt ..............................................................................................41 Programm 3.62 Eiffel: Generische Konzeptklasse für iterativ gelöstes Problem 41 Programm 3.63 Eiffel: Lösung zum ggT-Problem ...............................................43 Programm 3.64 Eiffel: ggT-Problem, abstrakt .....................................................44 Programm 3.65 Eiffel: ggT-Problem Variante 1 ...................................................44 Programm 3.66 Eiffel: ggT-Problem Variante 2....................................................45 Programm 3.67 Eiffel: Konzeptklasse für iterativ-imperativ gelöstes Problem ...47 Programm 3.68 Eiffel: Lösung zum ggT-Problem, erweitert ...............................48 Programm 3.69 Eiffel: ggT-Problem Variante 3 ...................................................48 Programm 3.70 Eiffel: ggT-Rechner als Problemlöser ........................................49 Programm 3.71 C++: Generische Konzeptklasse für Problem ............................51 Programm 3.72 C++: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem, Schnittstelle ............................................................................................51 Programm 3.73 C++: Lösung zum ggT-Problem, Schnittstelle ...........................53 Programm 3.74 C++: Lösung zum ggT-Problem, Implementation .....................53 Programm 3.75 C++: ggT-Problem, Schnittstelle ................................................53 Programm 3.76 C++: ggT-Problem, Implementation ...........................................54 Programm 3.77 C++: ggT-Rechner als Problemlöser ..........................................55 Programm 3.78 Java: Generisches Konzept-Interface für Problem .....................55 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 xxii Programmverzeichnis 4 Programm 3.79 Java: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem ..................................................................................................................56 Programm 3.80 Java: Lösung zum ggT-Problem .................................................56 Programm 3.81 Java: ggT-Problem ......................................................................56 Programm 3.82 Java: ggT-Rechner als Problemlöser ..........................................57 Programm 3.83 C#: Generische Konzeptklasse für Problem ...............................57 Programm 3.84 C#: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem ..................................................................................................................58 Programm 3.85 C#: Lösung zum ggT-Problem ...................................................58 Programm 3.86 C#: ggT-Problem ........................................................................58 Programm 3.87 C#: ggT-Rechner als Problemlöser .............................................59 Reihungen und Abstraktionen Programm 4.1 Component Pascal: Sortiertprüfung und binäres Suchen, rekursiv 3 Programm 4.2 Component Pascal: Partielle Sortiertprüfung, iterativ ....................4 Programm 4.3 Component Pascal: Binäres Suchen, iterativ, unstrukturiert ...........5 Programm 4.4 Eiffel: Sortiertprüfung iterativ; binäres Suchen, rekursiv ...............7 Programm 4.5 Eiffel: Testtreiber zu binärem Suchen ............................................9 Programm 4.6 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv; Schnittstelle mit generischen Funktionen ..............................................................12 Programm 4.7 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv; Schnittstelle mit generischer Klasse ......................................................................15 Programm 4.8 C++: Testtreiber zu binärem Suchen mit generischen Funktionen .................................................................................................................................16 Programm 4.9 C++: Testtreiber zu binärem Suchen mit generischer Klasse .......17 Programm 4.10 Java: Sortiertprüfung, iterativ; binäres Suchen, rekursiv ............18 Programm 4.11 Java: Testtreiber zu binärem Suchen ..........................................19 Programm 4.12 Miranda: Quicksort .....................................................................22 Programm 4.13 Component Pascal: Quicksort, rekursiv .....................................23 Programm 4.14 C++: Quicksort, rekursiv, Schnittstelle mit generischen Funktionen .............................................................................................................24 Programm 4.15 Component Pascal: Indexbereich mit maximaler Summe .........27 Programm 4.16 Eiffel: Indexbereich mit maximaler Summe ..............................28 Programm 4.17 C++: Indexbereich mit maximaler Summe, Schnittstelle ..........30 Programm 4.18 Java: Indexbereich mit maximaler Summe ................................31 Programm 4.19 Eiffel: Abstrakte generische Initialisierung von Reihungen vergleichbarer Elemente .........................................................................................33 Programm 4.20 Eiffel: Initialisierung gleitpunktzahliger Reihungen ..................34 Programm 4.21 Eiffel: Initialisierung von Reihungen von Zeichenketten ..........35 Programm 4.22 Eiffel: Abstrakter generischer Testtreiber zu Reihungen vergleichbarer Elemente ........................................................................................37 Programm 4.23 Eiffel: Abstrakter Testtreiber zu Reihungen von Zeichenketten .38 Programm 4.24 Eiffel: Abstrakter Testtreiber zu gleitpunktzahligen Reihungen 39 Programm 4.25 Eiffel: Testtreiber zu binärem Suchen in Reihungen von Zeichenketten .........................................................................................................40 Programm 4.26 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe in gleitpunktzahligen Reihungen ...............................................................................41 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Programmverzeichnis xxiii 5 6 7 8 9 27.9.12 Programm 4.27 C++: Generische Initialisierung von Reihungen vergleichbarer Elemente ................................................................................................................43 Programm 4.28 C++: Generischer Testtreiber zu Reihungen vergleichbarer Elemente ................................................................................................................44 Programm 4.29 C++: Testtreiber zu binärem Suchen in ganzzahligen Reihungen .................................................................................................................................45 Programm 4.30 C++: Testtreiber zu Indexbereich mit maximaler Summe in gleitpunktzahligen Reihungen ...............................................................................45 Programmeinheiten und -strukturen Programm 5.1 C++: using-Direktive ......................................................................7 Programm 5.2 C++: Namensraum mit Aliasnamen ...............................................8 Programm 5.3 C#: using-Direktive ........................................................................9 Programm 5.4 Component Pascal: Kunden-Lieferanten-Module ........................14 Programm 5.5 C++: Kunden-Lieferanten-Module ..............................................16 Klassen und objektorientierte Konzepte Typen und Datenkonzepte Ablaufsteuerung und algorithmische Konzepte Syntaktische und lexikalische Aspekte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 xxiv © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Programmverzeichnis 27.9.12 Beispielverzeichnis 1 2 3 4 5 6 7 8 9 Einführung Beispiel 1.1 Imperativ verbal: Fläche eines Rechtecks ...........................................6 Beispiel 1.2 Imperativ formal: Fläche eines Rechtecks ...........................................6 Beispiel 1.3 Funktional: Fläche eines Rechtecks ..................................................10 Beispiel 1.4 Miranda: Sieb des Eratosthenes .........................................................12 Beispiel 1.5 Prolog: Fläche eines Rechtecks .........................................................12 Beispiel 1.6 Regelbasiert: Fläche eines Rechtecks ................................................13 Einmal quer durch die Sprachen Funktional, rekursiv, iterativ, objektorientiert, abstrakt Reihungen und Abstraktionen Programmeinheiten und -strukturen Beispiel 5.1 Eiffel: Ace-Datei ..................................................................................6 Beispiel 5.2 Modulare Benutzungsbeziehung .......................................................14 Klassen und objektorientierte Konzepte Typen und Datenkonzepte Ablaufsteuerung und algorithmische Konzepte Syntaktische und lexikalische Aspekte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xxv xxvi © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Beispielverzeichnis 27.9.12 Leitlinienverzeichnis 1 2 3 4 5 6 7 8 9 Einführung Einmal quer durch die Sprachen Leitlinie 2.1 Vermeide Nebeneffekte .......................................................................4 Leitlinie 2.2 C++: Inkludiere nur bedingt ...............................................................7 Leitlinie 2.3 C++: Vermeide replizierte Konstanten ...............................................8 Leitlinie 2.4 Java: Vermeide replizierte Konstanten ...............................................9 Leitlinie 2.5 Bevorzuge Benutzung vor Mix-Ins ..................................................18 Leitlinie 2.6 C++: Vermeide unsichere C-Reihungen ...........................................20 Leitlinie 2.7 Ordne jedem Zweck eigene Größen zu ............................................21 Leitlinie 2.8 Vermeide leere catch-Blöcke ............................................................23 Leitlinie 2.9 Verwende Ausgabeparameter nur bei Prozeduren ............................25 Leitlinie 2.10 Component Pascal: Verberge Daten hinter Schnittstellen ..............27 Leitlinie 2.11 C*: Verberge Daten hinter Schnittstellen ........................................31 Leitlinie 2.12 C++: Lesbar statt kurz ....................................................................32 Leitlinie 2.13 C++: Denke an Redefinierbarkeit ...................................................33 Leitlinie 2.14 C#: Denke an Redefinierbarkeit .....................................................36 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Leitlinie 3.1 C*: Vermeide Sprunganweisungen ...................................................17 Leitlinie 3.2 Java: Bändige protected stark ...........................................................29 Leitlinie 3.3 Java: Bändige protected ....................................................................29 Reihungen und Abstraktionen Leitlinie 4.1 C++: Benutze Vektoren statt Reihungen ..........................................11 Programmeinheiten und -strukturen Leitlinie 5.1 C++: Vermeide using ..........................................................................7 Leitlinie 5.2 Java: Vermeide import ........................................................................8 Leitlinie 5.3 C#: Vermeide using .............................................................................9 Leitlinie 5.4 Module als ADSen ............................................................................14 Klassen und objektorientierte Konzepte Leitlinie 6.1 C++: Vermeide Verdecken in abgeleiteten Klassen ..........................16 Leitlinie 6.2 C++: Vermeide Überladen virtueller Funktionen .............................17 Leitlinie 6.3 C++: Vereinbare Elementfunktionen virtuell ...................................17 Leitlinie 6.4 Java: Benutze @Override .................................................................18 Leitlinie 6.5 Java: Vermeide Verwirrendes ...........................................................18 Typen und Datenkonzepte Leitlinie 7.1 C++: Vermeide Typkonversionen .......................................................9 Ablaufsteuerung und algorithmische Konzepte Syntaktische und lexikalische Aspekte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xxvii xxviii © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Leitlinienverzeichnis 27.9.12 Aufgabenverzeichnis 1 2 3 4 Einführung Aufgabe 1.1 Zahlen interpretieren ........................................................................27 Einmal quer durch die Sprachen Aufgabe 2.1 Hello World ......................................................................................12 Aufgabe 2.2 Abfrageorientierte Konversion .........................................................17 Aufgabe 2.3 Kommandoargumente ......................................................................26 Aufgabe 2.4 C++: Falsch getickt .....................................................................34 Aufgabe 2.5 Java: Falsch getickt ...........................................................................35 Aufgabe 2.6 Code verbessern ................................................................................38 Aufgabe 2.7 Datum ...............................................................................................39 Aufgabe 2.8 Englischkenntnisse nachweisen ........................................................39 Aufgabe 2.9 Problem schematisch lösen ...............................................................39 Aufgabe 2.10 Chaostheorie – Feigenbaums Periodenverdopplung ......................39 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Aufgabe 3.1 C++: Größter gemeinsamer Teiler, fehlerhaft ..................................11 Aufgabe 3.2 Java: Größter gemeinsamer Teiler, fehlerhaft ..................................13 Aufgabe 3.3 C#: Größter gemeinsamer Teiler, fehlerhaft .....................................14 Aufgabe 3.4 ggT- und kgV-Rechner ......................................................................15 Aufgabe 3.5 ggT-Funktion iterativ variiert ...........................................................15 Aufgabe 3.6 Stellenzahl ........................................................................................16 Aufgabe 3.7 Sprunganweisungen? Nein danke! ...................................................16 Aufgabe 3.8 Eiffel: ggT, oo-endrekursiv mit Wertsemantik .................................21 Aufgabe 3.9 C#: ggT, oo-iterativ, Referenztyp .....................................................32 Aufgabe 3.10 C#: ggT, oo-rr, Werttyp ...................................................................33 Aufgabe 3.11 Ablaufverfolgung einer ggT-Berechnung .......................................34 Aufgabe 3.12 ggT- und kgV-Kunde ......................................................................35 Aufgabe 3.13 Zahlenpaarklassen, effizienter ........................................................35 Aufgabe 3.14 Stellenzahl, objektorientiert ............................................................35 Aufgabe 3.15 Ziffernquadratsummenfolge ...........................................................35 Aufgabe 3.16 Entwürfe 3.3.2, 3.3.4 ......................................................................60 Aufgabe 3.17 ggT- und kgV-Problemlöser ............................................................60 Aufgabe 3.18 Stellenzahl, konkretisierte Abstraktion ...........................................60 Reihungen und Abstraktionen Aufgabe 4.1 Eiffel: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................8 Aufgabe 4.2 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................14 Aufgabe 4.3 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................16 Aufgabe 4.4 Java: Sortiertprüfung rekursiv, binäres Suchen iterativ ....................19 Aufgabe 4.5 C#: Sortiertprüfung, binäres Suchen ................................................20 Aufgabe 4.6 Eiffel: Quicksort, rekursiv ................................................................24 Aufgabe 4.7 C++: Quicksort, rekursiv mit generischer Klasse ............................24 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 xxix xxx Aufgabenverzeichnis 5 6 7 8 9 Aufgabe 4.8 Java: Quicksort, rekursiv ..................................................................25 Aufgabe 4.9 C#: Quicksort, rekursiv ....................................................................25 Aufgabe 4.10 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe ............29 Aufgabe 4.11 C++: Indexbereich mit maximaler Summe als Klassenfunktion ....30 Aufgabe 4.12 C++: Testtreiber zu Indexbereich mit maximaler Summe .............30 Aufgabe 4.13 Java: Indexbereich mit maximaler Summe ....................................31 Aufgabe 4.14 C#: Indexbereich mit maximaler Summe .......................................31 Aufgabe 4.15 C++: Abstrakte und konkrete Initialisierer und Testtreiber ............46 Aufgabe 4.16 C++: Testtreiber zu Klassenvarianten der Werkzeugkästen ...........46 Aufgabe 4.17 Java: Testtreiber mit wiederverwendbaren Teilen ..........................46 Aufgabe 4.18 C#: Testtreiber mit wiederverwendbaren Teilen ............................46 Programmeinheiten und -strukturen Aufgabe 5.1 Java: Getrennte Kompilation ..............................................................4 Aufgabe 5.2 C#: Getrennte Kompilation ................................................................4 Klassen und objektorientierte Konzepte Typen und Datenkonzepte Ablaufsteuerung und algorithmische Konzepte Syntaktische und lexikalische Aspekte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1 Einführung Aufgabe Beispiel 1 nur Bild 1 eine 1 (1) Leitlinie Programm 1 1 Es gibt Geschichte. Tabelle 1 Karl Marx (1818 – 1883) ...aber es gibt unzählig viele Arten, sie zu erzählen. Programmiersprache – Programm – Programmieren Ekkehard Jost Dieses Kapitel beleuchtet kurz verschiedene Aspekte von Programmiersprachen. In weitem Sinn ist eine Programmiersprache (programming language) eine Notation zur Darstellung von Softwaremodellen. In engem Sinn ist eine textuell notierte Programmiersprache eine formale Sprache zur Darstellung von Modellen, die Programme heißen und sich auf Rechnern ausführen lassen. Ein Programm (program) ist eine maschinell bearbeitbare Lösung eines informatischen Problems aus einem Anwendungsbereich. Programmieren bedeutet Programme erstellen. In weitem Sinn umfasst Programmieren Tätigkeiten wie Analysieren, Spezifizieren, Entwerfen, Implementieren und Testen; in engem Sinn reduziert es sich auf das Codieren einer Problemlösung in einer Programmiersprache. Bild 1.1 Entwickler – Programm – Rechner liest versteht programmiert schreibt Programmiersprache Programm Programm Programm läuft auf speichert bearbeitet führt aus Eine Programmiersprache ist intensional betrachtet eine Notation für Modelle, extensional betrachtet eine Gesamtheit von Programmen und Werkzeugen zum Bearbeiten dieser Programme. 1.1 Klassifikation von Programmiersprachen Tausende von Programmiersprachen wurden seit den 1940er Jahren entwickelt [Kin]. Sie werden teils nicht mehr, teils noch benutzt. Viele Sprachen werden weiter entwickelt. Auch wenn die Dynamik früherer Jahre nachgelassen hat, erscheinen immer wieder neue Sprachen. Wie soll sich der Informatiker im babylonischen Sprachenwirrwarr zurechtfinden? Zur Orientierung ordnen wir die Menge der Programmiersprachen nach den Kriterien (1) bis (8) und strukturieren sie durch Beziehungen zwischen Teilmengen. Nicht immer lässt sich eine Programmiersprache genau einem Aspekt eines Kriteriums zuordnen. Manche der folgenden Aspekte sind unscharf. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 1 – Seite 1 von 34 1–2 Wo? 1 Einführung (1) Abstraktionsebene Nah an der Maschine entspricht einer niederen Abstraktionsebene, nah an der Anwendung einer hohen. Maschinensprachen (machine language) sind durch Prozessortypen definiert. Unter Maschinencode oder Objektcode versteht man die interne ausführbare Darstellung eines Maschinenprogramms als Bitmuster. Assemblersprachen (maschinenorientierte Sprache, assembly language) sind symbolische, textuelle Darstellungen von Maschinensprachen. Ein Assemblerprogramm (Assemblercode) ist ein Programm in Assemblersprache. Höhere Programmiersprachen (Hochsprache, high level language) haben mächtigere Konstrukte als Maschinenbefehle, verbergen Details der Rechnerarchitektur, ermöglichen die Formulierung von Algorithmen oder allgemeiner von Problemlösungen unabhängig von einem bestimmten Prozessortyp, und orientieren sich an Bedürfnissen von Anwendungsbereichen. Ein Quellprogramm (Quellcode, Quelltext) ist ein Programm in Hochsprache. Maschinen- und Assemblersprachen wird es so lange geben wie Rechner mit von-neumannscher Architektur. Jedoch sind Maschinensprachen für Menschen praktisch unlesbar, sodass sie nicht als Programmiermittel taugen. Assemblersprachen sind etwas besser lesbar, fordern vom Programmierer aber viele Detailkenntnisse des Prozessortyps und bieten nur niedere, maschinenabhängige Abstraktionen. Daher scheiden sie heute trotz ihrer Problemlösungsmächtigkeit – alles was die Maschine kann, kann die Assemblersprache – für die Anwendungsprogrammierung aus. Eingesetzt werden Assemblersprachen nur noch von Spezialisten zur Programmierung hardwarenaher Teile von Betriebssystemen und z.B. von neuen Signalprozessoren, für die noch kein Hochsprachenübersetzer existiert. Dagegen wächst die Bedeutung von Hochsprachen. Wozu? (2) Anwendungsbereich Hochsprachen scheiden sich grob in vielseitig einsetzbare Mehrzwecksprachen (general purpose, multipurpose) und problemorientierte Sprachen für spezielle Anwendungsbereiche (special purpose, problem-oriented) wie: Naturwissenschaft und Technik: komplizierte Berechnungen mit wenigen numerischen Daten, Gleitpunktzahlen, Reihungen, Vektoren, Matrizen, Fallunterscheidungen und Wiederholungen (Fortran, Algol 60, APL) Wirtschaft: einfache Berechnungen mit vielen Daten, Massendatenverarbeitung, Festpunktzahlen, Zeichen, Zeichenketten, Datensätze in Dateien und Datenbanken, Berichte, Tabellenkalkulation (Cobol, Excel) Künstliche Intelligenz (KI; artificial intelligence, AI): symbolische Berechnungen, verkettete Listen (Lisp, Prolog) Lehre: Einfachheit, Lernbarkeit (Basic, Logo, Pascal) Systemprogrammierung: Betriebssysteme, hardwarenahe Sprachkonzepte, Effizienz, verteilte Systeme (Assemblersprachen, BCPL, Forth, C, Lis, Modula, Go) Kommandooberfläche von Betriebssystemen: schnelle Erweiterbarkeit (Unix-Shell, MS-DOS-Shell) Prozesssteuerung und Echtzeitdatenverarbeitung: Nebenläufigkeit (Forth, Pearl, Ada, Chill) Datenbankabfrage (SQL) Skriptsprachen (Awk, Tcl/Tk, Perl, Python, Ruby) Textformatierung und Seitenbeschreibung (Tex, Latex, PostScript, HTML, CSS, XML, DTD, XML-Schema) © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.1 Klassifikation von Programmiersprachen 1–3 Grafik und virtuelle Realität (Logo, Cg, VRML, X3D) Web (JavaScript, PHP, Flash, ActionScript, Curl, Ajax, XSLT, JSP, JSPSTL) Die Aufzählung ließe sich fortsetzen. Gegenläufige Trends sind zu beobachten: Während einerseits Mehrzwecksprachen ihr Einsatzspektrum ausweiten, entstehen andererseits für neue Anwendungsbereiche neue kleine Spezialsprachen. Seit den 1960er Jahren decken PL/I, Simula 67, Algol 68, Pascal, C und Nachfolgersprachen die großen Anwendungsbereiche Naturwissenschaft, Technik und Wirtschaft ab. C, Modula, Ada und Nachfolger wie C++, Oberon und Component Pascal sind auch in die Systemprogrammierung vorgedrungen. Seit den 1990er Jahren sprießen aus den Nährböden Multimedia und Web vielfältige Sprachentwicklungen. Wann? (3) Rolle im Softwareentwicklungsprozess Analyse- und Entwurfssprachen (Petri-Netze, UML) Spezifikationssprachen (Actor Model, Z, IDL, Eiffel, OCL, D, Spec#) Implementationssprachen (Component Pascal, Eiffel, C++, Java, C#) Ein Trend geht von konventionellen Implementationssprachen zu Sprachen, die frühe Softwareentwicklungsphasen unterstützen. Beispiele sind die Implementationssprachen Eiffel, D und Spec# mit eingebauten Spezifikationskonstrukten. Die Grenze zwischen Entwurfs- und Spezifikationssprachen ist unscharf. Was? (4) Programmierparadigma imperativ (Component Pascal, Eiffel, C++, Java, C#) funktional (Lisp, Scheme, ML, Miranda, Haskell, Gofer) gemischt imperativ und funktional (CLOS, Dylan, Scala) logikbasiert (Prolog) regelbasiert (OPS5) Den Begriff Programmierparadigma behandelt 1.2. Imperative Sprachen waren, sind und bleiben voraussichtlich dominierend. Wodurch? (5) Notation textuell als Folge von Zeichen aus einem Zeichensatz (Component Pascal, Eiffel, C++, Java, C#) grafisch als Diagramm mit Kästchen und Pfeilen oder Graph mit Knoten und Kanten (Petri-Netze) gemischt textuell und grafisch (UML) visuell (Visual Basic, Visual *) Der Trend geht von rein textuellen Notationen zu grafischen Notationselementen. In der objektorientierten Softwareentwicklung lassen sich Programmsysteme grafisch als Klassendiagramme entwerfen, die Klassenschnittstellen textuell-grafisch spezifizieren und aus attributierten Diagrammen unvollständige Quelltextschablonen erzeugen. Eine visuelle Programmiersprache (visual programming language) ermöglicht auf einfache Weise, grafische Benutzungsoberflächen (graphical user interface, GUI) zu erzeugen. Oft gestaltet der Entwickler die Oberfläche mit einer Art Grafikeditor (GUIBuilder), ein Werkzeug erzeugt daraus Quelltextschablonen. Umgekehrt verfährt ein BlackBox-Werkzeug, das aus Quelltext einfache Dialoge erzeugt. Wie weit? 27.9.12 (6) Ausführbarkeit nicht maschinell ausführbare Modelle, Entwürfe und Spezifikationen maschinell ausführbare Programme © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1–4 1 Einführung Die Grenze fließt. Nicht ausführbare Modelle dienen oft als Zwischenprodukte, aus denen manuell, rechnergestützt oder automatisch ausführbare Programme entstehen. Programme von Implementationssprachen gelten als maschinell ausführbar. Womit? (7) Wie? (8) Ausführendes Organ Software: Interpretierer, virtuelle Maschine (Java, C#) Hardware: Prozessor (Component Pascal, Eiffel, C++) Implementationskonzept Interpretation (Java, C#) Kompilation (Component Pascal, Eiffel, C++, Java, C#) Die Kriterien (6), (7) und (8) hängen zusammen. Die Unterscheidung bei (7) ist grob; eine differenziertere Sichtweise erlaubt das Modell hierarchisch geschichteter virtueller Rechner in Bild 1.2 [PrZ98] S. 81. Die Unterscheidung bei (8) ist nicht ausschließend, da manche Sprachen sowohl kompiliert als auch interpretiert werden, und z.B. Java und C# beide Techniken nutzen. Die Implementationskonzepte behandelt 1.4.2.3. Bild 1.2 Hierarchie virtueller Rechner Anwendung interpretiert Benutzereingaben ist implementiert durch Hochsprache Hochsprache interpretiert Quellprogrammtext ist implementiert durch Übersetzer und Laufzeitsystem Betriebssystem interpretiert Systemaufrufe ist implementiert durch Maschinenbefehle Firmware interpretiert Maschinenbefehle ist implementiert durch Mikrocodebefehle Hardware interpretiert Mikrocodebefehle ist implementiert durch physische Geräte 1.2 Programmierparadigmen Paradigma Ein Paradigma1 ist nach Thomas S. Kuhn eine allgemeine wissenschaftliche Leitidee, auf deren Basis eine Wissenschaftlergemeinschaft langfristig arbeitet.2 Paradigmen gehen nicht stetig ineinander über, sondern stehen unvergleichbar gegeneinander. Gelegentlich findet ein Paradigmenwechsel statt: Ein vorherrschendes Paradigma wird in einer wissenschaftlichen Revolution durch ein neues Paradigma abgelöst. Programmierparadigma Den Paradigmenbegriff in die Programmierwelt zu übernehmen ist problematisch, aber nicht unnütz. Die freie Enzyklopädie Wikipedia übertreibt mit einer dynamischen Liste von 15 oder 12 Programmierparadigmen, da diese nicht unvergleichbar gegeneinander stehen, sondern sich vielfach ergänzen.3 Deshalb sei hier eingeschränkt: 1 Griechisch: Beispiel, Vorbild, Muster, Abgrenzung. 2 Thomas S. Kuhn (1922 – 1996), britischer Wissenschaftstheoretiker, lieferte mit The Structure of Scientific Revolutions (1962) ein einflussreiches Werk. 3 http://de.wikipedia.org/wiki/Programmierparadigma (Zugriffe 2005-08-15, 2012-03-07). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.2 Programmierparadigmen 1–5 Ein Programmierparadigma (programming paradigm) umfasst die zentralen Prinzipien, Konzepte und Begriffe, die nur einer bestimmten Programmiertechnik zugrunde liegen. Verschiedene Programmierparadigmen unterscheiden sich wesentlich in ihren Prinzipien, Konzepten und Begriffen. Im Unterschied zu kuhnschen Paradigmen lösen sich Programmierparadigmen nicht durch Paradigmenwechsel ab, sondern koexistieren in ihren Anwendungsbereichen seit langem bis auf Weiteres. Zudem sind allgemeine Prinzipien in mehreren Paradigmen zu finden, etwa das Teile-und-herrsche-Prinzip (engl. divide and conquer, lat. divide et impera), nach dem man ein Problem in Teilprobleme zerlegt, die Teilprobleme löst und die Teilproblemlösungen zu einer Problemlösung zusammensetzt. Die wichtigsten Programmierparadigmen, nämlich das imperative, das funktionale, das logikbasierte, und das regelbasierte stellt das Folgende kurz zur Allgemeinbildung vor. Überblicke zu Programmierparadigmen geben viele Lehrbücher, etwa [ApL92], [Lou93], [Seb06], [Set97]. Zahlreiche Arten der Programmierung sind durch Adjektive und Subjektive charakterisiert: adaptive P., agentenorientierte P., datenstromorientierte P., defensive P., egolose P., evolutionäre P., extreme P., generative P., genetische P., intentionale P., konkatenative P., literarische P., sprachorientierte P., subjektorientierte P. Constraint-P., Paarp. Teils sind es Programmierstile, die sich einem Programmierparadigma unterordnen lassen, teils Programmierkonzepte für bestimmte Anwendungsbereiche, teils befassen sie sich mehr mit der Organisation des Programmierprozesses als dem Programm. Programmiersprachen unterstützen meist ein bestimmtes Programmierparadigma, oft mehrere Paradigmen durch sprachliche Ausdrucksmittel – Sprachkonstrukte. Entsprechend spricht man von imperativen, funktionalen, logikbasierten und regelbasierten Programmiersprachen. 1.2.1 Imperatives Programmierparadigma Prinzip Das imperative Programmierparadigma widerspiegelt direkt die praktisch realisierte von-neumannsche Rechnerarchitektur, der in der theoretischen Informatik die TuringMaschine als formales Modell für Berechnungen entspricht.1, 2 Zu den Prinzipien des imperativen Programmierparadigmas gehört, Programme als Befehlsfolgen aufzufassen, die von Automaten ausgeführt werden. Imperative Programme bestehen aus Daten (data) mit veränderbaren Zuständen (state), wobei die aktuellen Zustände in einem Speicher abgelegt sind; Befehlen (instruction), die von Prozessoren ausgeführt werden, wobei die Ausführung (execution) eines Befehls einen Effekt bewirkt, nämlich Zustände von Daten verändert. 1 John von Neumann (1903 – 1957), genialer ungarisch-US-amerikanischer Mathematiker, begründete die Spieltheorie, trug zu vielen Gebieten wie Mengentheorie, Quantenmechanik, Differenzialgleichungen, Rechnerarchitektur bei. 2 Alan Turing (1912 – 1954), britischer Mathematiker und Logiker, leistete Herausragendes für die theoretische Informatik und die Kryptanalyse; trug im Zweiten Weltkrieg wesentlich zur Entschlüsselung des deutschen Enigma-Codes und damit zum Sieg der Alliierten über das Naziregime bei; wurde durch Verurteilung zu zwei Jahren Gefängnis wegen homosexueller Handlungen in den Suizid getrieben. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1–6 Beispiel 1.1 Imperativ verbal: Fläche eines Rechtecks Konzept und Begriff 1 Einführung Daten: Drei Zahlen namens width, height und area. Befehle: Schreibe den Wert 2 in die Zahl width. Schreibe den Wert 3 in die Zahl height. Schreibe das Produkt der Werte von width und height in die Zahl area. Zu seinen Konzepten gehört, Daten als Ansammlungen einzelner Variablen zu vereinbaren; Variablen durch Zuweisungen (assignment) an Werte (value) zu binden; Befehle linear angeordnet aufzuschreiben und sequenziell nacheinander auszuführen; Befehlsarten zu unterteilen in solche, die auf Variablen lesend oder schreibend zugreifen, und solche, die den Ablauf einer Befehlsfolge steuern. Neben anderen Bezeichnungen für Befehl wie Instruktion, Kommando, Anweisung gehören zu seinen Begriffen z.B. Datenbefehl, Lesezugriff, Schreibzugriff, Sprungbefehl, Steueranweisung. Werte können Typen zugeordnet sein, die Operationen auf diesen Werten definieren. Variablen können statisch oder dynamisch typisiert, d.h. an Typen gebunden sein. Die Anwendung von Operationen auf Variablen führt zu Ausdrücken (expression). Da im imperativen Programmierparadigma ein Programm einen Algorithmus oder mehrere zusammenwirkende Algorithmen beschreibt, heißt es auch algorithmisches Programmierparadigma. Da sich ein Programm auch als Folge von Zuständen beschreiben lässt, heißt es auch zustandsorientiertes Programmierparadigma. Das Programmieren mit Zusicherungen (assertion) betont diese Sicht. Bild 1.3 Zustandsübergangsdiagramm Beispiel 1.2 Imperativ formal: Fläche eines Rechtecks Daten Vorzustand Daten Nachzustand Befehl Daten: width, height, area : FLOAT Befehle: 2 → width 3 → height width * height → area Zustände: width = 2 height = 3 area = width * height area = 6 Im Beispiel 1.2 ist „→“ das Zuweisungssymbol, wobei die Zuweisung in der in diesem Kulturkreis üblichen Leserichtung von links nach rechts zu lesen ist; „=“ ist das jahrhundertealte mathematische Gleichheitssymbol. Stil Strukturiert Die folgenden Programmierstile (programming style) sind spezielle Ausprägungen des imperativen Paradigmas. Obwohl manchmal als eigenständige Paradigmen bezeichnet, stehen sie nicht gegeneinander, sondern bauen aufeinander auf. Strukturiertes Programmieren (structured programming) bedeutet, Algorithmen mit Anweisungsfolgen, Auswahl- und Wiederholungsanweisungen und Prozeduren zu formulieren. Jedes Konstrukt zur Ablaufsteuerung hat genau je einen Ein- und Ausgang. Das ist mehr als auf Sprunganweisungen (goto, if ... goto) zu verzichten! Bild 1.4 Struktogramm Strukturiertes Programmieren entstand in den 1960er Jahren zusammen mit der Top-Down-Algorithmenentwurfsmethode schrittweises Verfeinern (stepwise refine- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.2 Programmierparadigmen Prozedural Bild 1.5 Prozeduren mit Benutzung und Schachtelung 1–7 ment) und Verifikationsmethoden wie der axiomatischen Semantik, um Probleme undurchschaubaren Spaghetticodes zu bewältigen. Prozedurales Programmieren (procedural programming) bedeutet, ein Problem in Teilprobleme zu zerlegen, diese durch Prozeduren und das Problem durch Zusammenwirken der Prozeduren zu lösen. Prozeduren stehen in Benutzungs- oder Aufrufbeziehungen zueinander. Eine Prozedur ist eine Abstraktion von Anweisungen. Sie bietet eine Schnittstelle (interface) aus einem Namen und einer Parameterliste, hinter der sie eine Implementation aus lokalen Daten und Anweisungen verbirgt (kapselt und schützt). Prozeduraufrufe (procedure call) bewirken Effekte. Textuelle Schachtelung (nesting) von Prozeduren schränkt ihre Sichtbarkeit ein. Prozedurales Programmieren entstand in den 1950er Jahren zwecks Wiederverwendung von Codestücken. Während die Algorithmen prozedural strukturiert sind, fehlt eine entsprechende Strukturierung global vereinbarter und überall sichtbarer Daten. Name (Parameterliste) P Q lokale Daten Anweisungen call Q call R R S T U globale Daten call S call S Modular Bild 1.6 Module mit Benutzung und Schachtelung Modulares Programmieren (modular programming) bedeutet, ein Problem in Teilprobleme zu zerlegen, diese durch Module und das Problem durch Zusammenwirken der Module zu lösen. Module stehen in Benutzungs- oder Kunde-LieferantBeziehungen zueinander. Ein Modul hat einen Namen und bietet eine Schnittstelle, die vorzüglich aus Prozeduren besteht, synonym oft Operationen genannt. Modulares Programmieren kennt keine global vereinbarten, sondern nur gekapselte Daten, die aber nicht geschützt sein müssen. Schachtelung von Modulen kommt vor, ist aber weniger wichtig als die Benutzungsbeziehung. Modulares Programmieren entstand in den 1970er Jahren, um Probleme großer Softwaresysteme zu bewältigen. Name gekapselte Daten Operation A B call B.P call C.P C D E F Operation ADS 27.9.12 Programmieren mit abstrakten Datenstrukturen: Ein Modul ist vorzüglich eine abstrakte Datenstruktur (abstract data structure, ADS), es verbirgt hinter seiner Schnittstelle eine Implementation aus gekapselten und geschützten konkreten Daten © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1–8 ADT 1 Einführung und Algorithmen der Operationen, die auf die Daten zugreifen. Eine ADS ist ausschließlich durch ihr extern beobachtbares Verhalten definiert, spezifiziert durch die Menge anwendbarer Operationen, wobei jede Operation durch ihre Signatur und Aufrufkonvention und ihren Effekt beschrieben ist. Programmieren mit ADSen kennt keine überall sichtbaren Daten, Zustände von ADSen sind nur durch Operationsaufrufe abfragbar. Programmieren mit abstrakten Datentypen bedeutet, Probleme mit Exemplaren abstrakter Datentypen zu lösen, die in Benutzungsbeziehungen zueinander stehen. Ein abstrakter Datentyp (abstract data type, ADT) ist eine zu einem Typ abstrahierte ADS. Die Typeigenschaft bedeutet, dass sich von einem ADT beliebig viele Exemplare bilden lassen, die alle ADSen mit denselben Operationen und demselben Verhalten, aber jeweils eigenen Zuständen und internen Daten darstellen. Programmieren mit abstrakten Datentypen entstand in den 1970er Jahren zusammen mit Methoden zur Spezifikation und Verifikation von Programmkomponenten. Bild 1.7 Abstrakter Datentyp mit Exemplaren Typ Exemplar 1 Objektorientiert Exemplar 2 Exemplar 3 Objektorientiertes Programmieren (object-oriented programming) bedeutet, Probleme mit Objekte genannten Exemplaren von Klassen zu lösen. Klassen sind mehr oder weniger implementierte ADTen, die in Benutzungs- und Vererbungsbeziehungen zueinander stehen können. Ein zentrales Konzept ist Polymorphie und dynamisches Binden: Namen können sich auf dynamisch erzeugte und gebundene Objekte verschiedener Klassen beziehen. Wie bei Modulen ist Schachtelung von Klassen eher marginal interessant. Objektorientiertes Programmieren entstand in den 1960er Jahren für Simulationsanwendungen und in den 1970er Jahren zusammen mit grafischen Bildschirmen und Arbeitsplatzrechnern. Bild 1.8 Klassen mit Benutzung und Vererbung Oberklasse Kunde Klasse Lieferant Unterklasse Komponentenorientiert Komponentenorientiertes Programmieren (component-oriented programming) ermöglicht, unabhängig voneinander entwickelte wiederverwendbare Pro- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.2 Programmierparadigmen 1–9 grammeinheiten bei Bedarf dynamisch zu binden und zu laden. Komponenten sind oft Ansammlungen von Modulen und Klassen. Komponentenorientiertes Programmieren entstand in den 1990er Jahren, um Softwaresysteme flexibler zu gestalten. Bild 1.9 Komponenten your_component.dll her_component.dll dynamisch gebunden dynamisch gebunden my_prog.exe statisch gebunden Generisch Generisches Programmieren (generic programming) ermöglicht, Programmeinheiten, Algorithmen und Datenstrukturen so verallgemeinert zu definieren, dass sie für verschiedene Typen verwendbar sind. Die Definition einer generischen Programmeinheit ist mit formalen generischen Parametern versehen, die bei jeder Verwendung der Programmeinheit durch aktuelle konkrete Typen ersetzt werden. Generisches Programmieren entstand in den 1980er Jahren zwecks Wiederverwendung von Programmeinheiten. Bild 1.10 Generische Einheit mit Konkretisierungen Einheit [Typ] Einheit [FLOAT] Nebenläufig Einheit [SET] Einheit [LIST] Sprache Nebenläufiges Programmieren (concurrent programming) bedeutet, Probleme durch Strukturieren in mehrere sequenzielle Abläufe zu lösen. Oft sind Prozesse die sequenziellen Ablaufeinheiten. Jeder Prozess löst ein Teilproblem. Die Prozesse konkurrieren um benötigte Betriebsmittel; zur Problemlösung kooperieren sie, synchronisieren sich und kommunizieren miteinander. Nebenläufiges Programmieren entstand in den 1960er Jahren im Bereich Betriebssysteme. Aspektorientiertes Programmieren (aspect-oriented programming) versucht, Aspekte voneinander zu trennen, die quer zu konventionellen Struktureinheiten (Prozedur, Modul, Klasse) auftreten. Beispiele dafür sind Fehler- und Ausnahmebehandlung, Protokollierung und Persistenz. Ziel ist, solche bisher über mehrere Struktureinheiten verstreuten Aspekte besser zu kapseln. Aspektorientiertes Programmieren entsteht seit den 1990er Jahren, um Wartbarkeit und Wiederverwendbarkeit von Softwaresystemen zu erhöhen. Einige wichtige imperative Programmiersprachen sind in 1.6 und Bild 1.13 vorgestellt. 1.2.2 Funktionales Programmierparadigma Prinzip Das funktionale Programmierparadigma beruht auf Funktionen im mathematischen Sinn, dem Lambdakalkül und der Rekursionstheorie. Mathematische Funktionen sind Abbildungen der Form Aspektorientiert f : M → N, x |→ f(x) = y mit einem Definitionsbereich M, einem Wertebereich N und einer Abbildungsvorschrift x |→ f(x), die oft durch einen Ausdruck darstellbar ist. Zu den Prinzipien des funktio- 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 10 1 Einführung nalen Programmierparadigmas gehört, Programme als Funktionen aufzufassen und aus Funktionen aufzubauen, wobei Funktionsdefinitionen Funktionen definieren und Funktionsanwendungen Funktionsdefinitionen anwenden. Man spricht auch vom applikativen Programmierparadigma, da man Probleme durch Funktionsanwendungen löst. Beispiel 1.3 Funktional: Fläche eines Rechtecks Konzept und Begriff Funktionsdefinition: area : +2 → +, area (width, height) = width * height. Funktionsanwendung: area (2, 3). Wert: 6. Zu seinen Konzepten gehört: Eine Funktion ist eine Abstraktion eines Ausdrucks. Die Auswertung eines Ausdrucks liefert einen Wert. Der Wert eines Ausdrucks hängt nur von den Werten seiner Teilausdrücke ab, sofern vorhanden. Ein funktionales Programm besteht aus einer Ansammlung von Funktionsdefinitionen. Ein Programmablauf entsteht durch eine Funktionsanwendung, in der Anfangswerte angegeben werden. Die Argumente der Funktionen heißen Parameter. In Funktionsdefinitionen erscheinen formale Parameter als Namen, in Funktionsanwendungen erscheinen aktuelle Parameter als Ausdrücke. Eine Funktionsdefinition definiert eine Funktion durch einen Namen, eine Formalparameterliste und einen Ausdruck. Eine Funktionsanwendung oder ein Funktionsaufruf ist ein Ausdruck, zu dessen Auswertung die aktuellen Parameter ausgewertet und ihre Werte in den definierenden Ausdruck eingesetzt werden, dieser ausgewertet und der erhaltene Wert als Ergebniswert in die Anwendungsstelle eingesetzt, m.a.W. als Rückgabewert an die Aufrufstelle zurückgegeben wird. Parameter können statisch oder dynamisch typisiert sein. Ein Ausdruck setzt sich aus Funktionsanwendungen zusammen. Gewisse Grundfunktionen sind vorgegeben. Funktionen sind Werte erster Klasse, d.h. sie können als Parameter, Ergebnisse und Teil von Datenstrukturen auftreten. Damit lassen sich Funktionen zu höheren Funktionen komponieren und Operationen auf Datenstrukturen definieren. Rekursion Ein wesentliches Konzept des funktionalen Programmierparadigmas ist Rekursion in Form rekursiver Funktionsanwendungen. Bei direkter Rekursion enthält der definierende Ausdruck eine Anwendung der definierten Funktion. Bei indirekter Rekursion mit den Funktionen f1,.., fn enthält der definierende Ausdruck von fi eine Anwendung von fi mod n + 1. Bezugstransparenz Ein weiteres wichtiges Konzept ist Bezugstransparenz (referential transparency), was allgemein fordert, dass Zugriffe unabhängig von Implementationen sind. Hier bedeutet es, dass das Ergebnis einer Funktionsanwendung nur von der Funktion und den aktuellen Parametern abhängt, nicht von der Reihenfolge ihrer Auswertung, der Stelle oder dem Zeitpunkt der Anwendung. Sprache Zu den funktionalen Programmiersprachen zählt als erste Lisp (List Processor, McCarthy, 1960); spätere Entwicklungen sind Scheme (Sussman, Steele, 1975), Common Lisp (Steele, 1984), CLOS (Common Lisp Object System, Bobrow, 1988), ML (MetaLanguage, Milner, Tofte, Harper, 1990), Miranda (Turner, 1986), Haskell (Hudak, Fasel, 1992), Gofer (Jones, 1991). Anwendungsbereiche sind vor allem die Künstliche Intelligenz und die Wissensverarbeitung. Beeinflusst von CLOS entstehen Sprachen wie Dylan (Apple Computer, Anfang 1990er Jahre) und Scala (Odersky, 2001), die das funktionale Paradigma mit Objektorientierung verbinden. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.2 Programmierparadigmen Tabelle 1.1 Funktional: Fläche eines Rechtecks Funktional versus imperativ Programmiersprache 1 – 11 Funktionsdefinition Funktionsanwendung Lisp (area (LAMBDA (width height) width * height)) (area 2 3) Scheme (define (area width height) (* width height)) (area 2 3) ML fun area (width, height : real * real) : real = width * height; area (2, 3) Miranda area (width, height) = width * height area (2, 3) Worin unterscheidet sich das funktionale vom imperativen Programmierparadigma? Funktionen trennen klar zwischen hereinkommenden Werten – Parametern – und hinausgehenden Werten – Ergebnissen. Dagegen können bei imperativen Prozeduren, die auf globale Daten zugreifen oder Referenzparameter benutzen, dieselben Daten als Ein- und Ausgabegrößen auftreten. Funktionen erhalten alle Werte durch Parameter. Es gibt keine Zustände, keine Variablen, keine Zuweisungen. Daher kann kein Funktionsergebnis von einem Zustand abhängen. Funktionen können keine Nebeneffekte bewirken. Dagegen sind Zustände beim imperativen Programmieren wesentlich. Funktionen in imperativen Sprachen können Nebeneffekte (side-effect) bewirken, d.h. sie können nicht nur ein Ergebnis zurückgeben, sondern auch Zustände globaler Variablen verändern. Entsprechendes gilt für Ausdrücke: Im funktionalen Paradigma sind Ausdrücke stets nebeneffektfrei, im imperativen Paradigma können sie nebeneffektbehaftet sein. Nebeneffektfreiheit ist ein Aspekt der Bezugstransparenz: Funktionen sind von Natur aus nebeneffektfrei. Oft sind sie auch bezugstransparent insofern, als ihre Ergebnisse nicht von Anwendungsstellen und Auswertungsreihenfolgen der Parameter abhängen. Dagegen können beim imperativen Programmieren nebeneffektbehaftete Ausdrücke die Bezugstransparenz von Routinen, d.h. Prozeduren und Funktionen, stören. Es gibt keine Anweisungen, keine Steueranweisungen, keine Schleifen. Bedingte Ausdrücke und rekursive Funktionsanwendungen erledigen die Berechnungen. Speicher wird implizit verwaltet, unbenutzte Speicherbereiche werden automatisch freigegeben. Dagegen muss im imperativen Programmierparadigma der Programmierer die Speicherarten globaler Speicher, Keller und Halde kennen und Speicherbereiche durch Vereinbaren statischer oder kellerdynamischer Variablen und Erzeugen und ggf. Freigeben dynamischer Variablen explizit selbst verwalten. Wer das funktionale Programmierparadigma kennt, wird dessen Vorteile beim imperativen Programmieren nutzen, sorgfältig mit Funktionen arbeiten, nebeneffektfreie Funktionen bevorzugen und nebeneffektbehaftete Funktionen vermeiden. Problemlösungsmächtigkeit Der Church-Turing-These zufolge lassen sich im Lambdakalkül und mit einer TuringMaschine dieselben Probleme lösen.1 Daher sind rein funktionale Programmiersprachen Turing-vollständig, d.h. sie können alle Berechnungen beschreiben, die eine Turing-Maschine ausführen kann. Damit können funktionale Sprachen alle mit imperativen Sprachen lösbaren Probleme lösen, allerdings – und hier liegt ihr Nachteil – oft weniger effizient. 1 Alonzo Church (1903 – 1995), US-amerikanischer Mathematiker, trug wesentlich zur theoretischen Informatik bei, entwickelte den Lambdakalkül, entdeckte die Unentscheidbarkeit formaler Probleme. Nach der Church-Turing-These sind viele Berechnungsmodelle äquivalent. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 12 Ausdrucksstärke 1 Einführung Ein Vorteil funktionaler Sprachen ist dagegen ihre Einfachheit und Ausdrucksstärke. Viele Problemlösungen erscheinen mit funktionalen Sprachen kompakter und prägnanter als mit imperativen Sprachen. Während etwa für das Sieb des Eratosthenes zur Bestimmung der Primzahlen eine imperative Lösung minimal rund 20 Anweisungen, darunter einige Schleifen und eine Fallunterscheidung benötigt, genügen einer funktionalen Lösung zwei Zeilen.1 Beispiel 1.4 Miranda: Sieb des Eratosthenes sieve (h : t) = h : sieve [ n | n <- t; n mod h ~= 0 ] primes = sieve [ 2 .. ] 1.2.3 Logikbasiertes Programmierparadigma Prinzip Das logikbasierte (prädikative, wissensbasierte) Programmierparadigma beruht auf der Prädikatenlogik. Zu seinen Prinzipien gehört, Programmabläufe als Beweise in einem System logischer Aussagen aufzufassen. Man beschreibt ein Problem durch Konzept und Begriff Sprache eine Menge von Eigenschaften (predicate) in Form von Fakten (fact), d.h. gültigen Prädikaten, und Regeln (rule), d.h. Implikationen von Fakten, um neue Fakten zu gewinnen, und Abfragen (goal, query), die bestimmte Eigenschaften prüfen und mit yes oder no antworten, ggf. auch mit den für yes zutreffenden Belegungen der Variablen in der Abfrage. Zu seinen Konzepten gehört: Ein Regelinterpretierer versucht, aus den Fakten und Regeln Antworten auf die Abfragen zu gewinnen. Dabei spielen Horn-Verfahren und Horn-Klauseln eine wichtige Rolle. Die bekannteste logikbasierte Programmiersprache ist Prolog (programmation en logique, Colmerauer, Kowalski, Roussel, 1972). Anwendungsbereiche sind die Künstliche Intelligenz und die Wissensverarbeitung. Beispiel 1.5 Prolog: Fläche eines Rechtecks Regeln: valid (width, height) :- (width > 0), (height > 0). (area = width * height) :- valid (width, height). Fakten: (width = 2). (height = 3). Abfrage: Antwort: Abfrage: Antwort: ?- (area = 5). no ?- (area = x). x=6 Das Symbol „:-“ entspricht dem Implikationspfeil „⇐“, wobei der rechte Ausdruck den linken impliziert. In der zweiten Abfrage ist x eine Variable, für die der Regelinterpretierer einen passenden Wert findet. 1.2.4 Regelbasiertes Programmierparadigma Prinzip Das regelbasierte Programmierparadigma kombiniert logische und imperative Elemente. Zu seinen Prinzipien gehört, Programme als Zustände und Zustandsübergangsregeln aufzufassen. Man beschreibt ein Problem durch einen Startzustand und einen Zielzustand und eine Menge von Wenn-Dann-Regeln der Form „Wenn Bedingung gilt, dann Aktion ausführen“. 1 Eratosthenes von Kyrene (ca. 282 – 202 v.u.Z.), griechischer Gelehrter in Alexandria, arbeitete zur Philologie, Grammatik, Literaturgeschichte, Mathematik, Chronologie, Astronomie und Geographie. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.2 Programmierparadigmen Konzept und Begriff 1 – 13 Zu seinen Konzepten gehört: Der Problembereich wird durch Daten in Form von Variablen modelliert. Zustände setzen sich aus Bedingungen an Variablen zusammen. Aktionen sind Zustandsveränderungen wie Merken und Streichen von Bedingungen. Beispiel 1.6 Regelbasiert: Fläche eines Rechtecks Startzustand: width = 0, height = 0, area = width * height, Ziel (width > 0, height > 0) Wenn-Dann-Regeln: Wenn width = 0 und Ziel (width > 0, height > 0) dann merke (width = 2, Ziel (height > 0)) streiche (width = 0, Ziel (width > 0, height > 0)) Wenn height = 0 und Ziel (width > 0, height > 0) dann merke (height = 3, Ziel (width > 0)) streiche (height = 0, Ziel (width > 0, height > 0)) Wenn width = 0 und Ziel (width > 0) dann merke (width = 2) streiche (width = 0, Ziel (width > 0)) Wenn height = 0 und Ziel (height > 0) dann merke (height = 3) streiche (height = 0, Ziel (height > 0)) Sprache Regelbasierte Programmiersprachen treten in vielen Variationen auf; ein Beispiel ist OPS5. Anwendungsbereiche sind die Wissensverarbeitung und die Mathematik. Regelbasierte Programmiersprachen hängen mit Produktionensystemen zusammen. „Ein Produktionensystem besteht aus einer globalen Datenbasis, die die Fakten des Problems enthält, einer Regelbasis mit den auf der Datenbasis operierenden (Produktions)Regeln und einem Regelinterpreter (inference engine), der die Systemaktivitäten mittels eines Erkenne-Agiere-Zyklus (recognize-act cycle) steuert. Produktionensysteme eignen sich zur Lösung von Problemen, bei denen das zu programmierende Wissen in natürlicher Weise in Regelform vorkommt oder bei denen die Programmkontrolle sehr komplex ist.“1 Für mathematische Anwendungen ist es oft zweckmäßig, Programme als Anwendung von Umformungsregeln zu formulieren. Die Regeln werden mit Mustererkennungsalgorithmen (pattern matching) angewandt. So ist etwa der Kern des Computeralgebrasystems Mathematica aufgebaut.2 1.2.5 Fazit Imperativ Im imperativen Programmierparadigma muss der Programmierer das „Wie?“ einer Problemlösung festlegen, indem er einen vollständigen Lösungsweg in Form eines Algorithmus entwirft und in ein Programm umsetzt. Das Programm läuft i.d.R. deterministisch ab. Deklarativ Das funktionale Programmierparadigma kennt keine Algorithmen, keine Variablen, keine Anweisungen. Funktionsdefinitionen sind deklarativ, nicht imperativ. Aber das „Wie?“ einer Problemlösung ist festgelegt, da Ausdrücke i.d.R. deterministisch ausgewertet werden. 1 F. J. Radermacher, Th. Kämpke, Th. Rose, K. Tochtermann, T. Richter: „Management von nicht-explizitem Wissen: Noch mehr von der Natur lernen“ Abschlussbericht Teil 2 Wissensmanagement: Ansätze und Erfahrungen in der Umsetzung. Hrsg: Forschungsinstitut für anwendungsorientierte Wissensverarbeitung (FAW) Ulm, erstellt im Auftrag des Bundesministeriums für Bildung und Forschung (bmb+f) (März 2001) S. 65 von 121 S.; http://www.faw-neu-ulm.de/ sites/default/files/BMBF_Studie_Teil_2.pdf (Zugriff 2012-03-09). 2 http://wwwmath1.uni-muenster.de/u/mathinfo/kommvor/ws9899/cleantex/Kommentare/ node20.html (Zugriff 2005-08-25). 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 14 1 Einführung Deskriptiv Im Unterschied dazu muss der Programmierer im logik- und im regelbasierten Programmierparadigma nur das „Was?“ einer Problemlösung beschreiben, die Eigenschaften der Lösung als Ziel. Eigenschaften, Fakten, Regeln sind deskriptiv. Nichtdeterministisch Gemeinsames Merkmal ist der Nichtdeterminismus: Ein Regelinterpretierer kann die Regeln in beliebiger, nichtdeterministischer Reihenfolge anwenden. Die Programme selbst legen nicht fest, ob und wie das Ergebnis zu berechnen oder das Ziel zu erreichen ist. Allerdings arbeiten viele Regelinterpretierer praktisch mit deterministischen Strategien, nach denen sie die Regeln anwenden. Die verschiedenen Programmierparadigmen entsprechen damit verschiedenen Abstraktionsebenen, die von der niederen Abstraktionsebene des imperativen Paradigmas über die mittlere Ebene des funktionalen, deklarativen Paradigmas zur hohen Ebene der deskriptiven Paradigmen reichen. 1.3 Abstraktionsebenen in imperativen Sprachen Abstraktion Abstraktion ist ein Kernthema der Informatik. Schon der frühgeschichtliche Mensch abstrahierte von den ihn umgebenden Dingen und klassifizierte seine Begriffe. Als der aus dem Alten Testament bekannte Noah sein Schiff füllte, wusste er Lebewesen in Pflanzen und Tiere zu unterscheiden und kannte als Tiere Ziegen, Schweine, Kamele und Elefanten. Griechische Philosophen wie Heraklit, Platon, Aristoteles befassten sich mit Abstraktion. Klassifikation und Bestandteilbeziehung Heute bieten objektorientierte Programmiersprachen Vererbung als Mittel, um in die Software abgebildete Begriffe zu klassifizieren. Orthogonal zur Klassifikation steht die ebenso wichtige Bestandteilbeziehung. Das Zerlegen eines Ganzen in seine Teile ist eine Form des Konkretisierens, das Zusammenfassen einzelner Dinge zu einem einheitlichen Ganzen eine Form des Abstrahierens. Objektorientiertes Modellieren arbeitet mit Aggregation und Komposition, objektorientierte Programmiersprachen bilden Bestandteil- auf Benutzungsbeziehungen ab. Imperative Programmiersprache Das Folgende bezieht sich auf das imperative Programmierparadigma. Zu diesem Abschnitt siehe auch [War02] Chapter 23. Die Entwicklung der Programmiersprachen zeigt einen Trend von niederen zu immer höheren Abstraktionsebenen. Ausgehend von Sprachkonzepten, die Merkmale der Maschine widerspiegeln, sucht man stets nach abstrakteren Sprachkonzepten, die oft wiederkehrende Lösungsmuster extrahieren und es ermöglichen, Anwendungsprobleme noch besser angemessen auszudrücken. (1) (2) (3) Von-neumannsche Rechnerarchitektur Speicher als Reihung von Speicherzellen für Bytes Register Maschinensprache mit Befehlen zum Lesen und Schreiben von Speicherzellen, Manipulieren von Registerinhalten und Steuern des Programmablaufs Maschinennahe Ebene Variablen mit Namen statt Adressen Zuweisung (Wert → Variable) Marken vor Anweisungen (Marke : Anweisung) Bedingte Sprunganweisungen (if Bedingung goto Marke) Ausdrucksstrukturierung Beliebig komplexe, rekursiv aufgebaute Ausdrücke zur Berechnung von Werten Bezugstransparenz © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.3 Abstraktionsebenen in imperativen Sprachen (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) 27.9.12 1 – 15 Typisierung Datentyp als Wertemenge mit Operationen darauf Typbindung von Variablen und Ausdrücken Sprachdefinierte einfache Datentypen für Zahlen und Zeichen Anweisungsstrukturierung Strukturierte Anweisungen für Sequenzen, Alternativen (if-then-else, case) und Iterationen (while, until, for) Datenstrukturierung Strukturierte Datentypen, die sich rekursiv aus anderen Datentypen zusammensetzen, wie - Reihungen und - Verbunde Programmiererdefinierte Datentypen durch Typkonstruktoren Algorithmenabstraktion Prozeduren Parameter Rekursive Prozeduraufrufe Datenabstraktion ADSen und ADTen In benannten Einheiten gekapselte Daten und Algorithmen Schnittstelle und Implementation getrennt Konkrete Daten hinter der Schnittstelle verborgen Schnittstelle auf Operationen beschränkt Abstrakte Daten durch Effekt von Zugriffsoperationen definiert Verhaltensabstraktion Vererbung oder Erweiterung zwischen ADTen macht diese zu Klassen Klassifizieren durch Generalisieren und Spezialisieren Abstrakte Klassen: nur partiell implementiert Polymorphie: Objekte austauschbar Generizität Abstrahieren von verwendeten Datentypen Parametrisieren von Programmeinheiten mit Typen Bibliotheken (library) Fertige benutzbare Lösungen für bekannte Teilprobleme Erweitern die Ausdrucksstärke von Sprachen Entwurfsmuster (design pattern) Anpassbare Lösungsstrukturen für ähnlich wiederkehrende Teilproblemarten Erweitern die Fähigkeiten von Programmierern Rohbauten (framework) Unvollständige erweiterbare Lösungen von Problemarten, zu denen Teilprobleme durch Erweiterungen zu lösen sind Erweitern die Fähigkeiten von Entwicklerteams und Firmen © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 16 1 Einführung 1.4 Soll und Ist von Programmiersprachen Prinzipiell sind bei Programmiersprachen zwei Aspekte zu unterscheiden: Die Beschreibung einer Programmiersprache umfasst die syntaktischen, semantischen und pragmatischen Eigenschaften damit formulierter Programme. Sprachbeschreibungen bestehen aus Dokumenten, die Texte und Grafiken enthalten. Die Implementation einer Programmiersprache umfasst die Umformung damit formulierter Quell- in Zielprogramme und ggf. deren Ausführung. Sprachimplementationen bestehen aus Werkzeugen zum Erstellen, Umformen, Ausführen und Testen von Programmen. Bild 1.11 gibt einen Überblick zu verschiedenen Teilaspekten. Weitere Aspekte sind Vielfalt und Einheit sowie Umgebungen von Programmiersprachen. Bild 1.11 Aspekte von Programmiersprachen Programmiersprache Beschreibung Pragmatik menschliche Pragmatik Implementation Semantik mechanische Pragmatik dynamische Semantik Syntax statische Semantik Syntax Lexik Darstellung in der Zielsprache Zahlen Zeichen Befehle Werkzeuge Editor Browser GUI-Builder Übersetzer Kompilierer 1.4.1 Binder Lader Laufzeitsystem Debugger Interpretierer Beschreibung von Programmiersprachen Die Beschreibung von Programmiersprachen teilt sich in die Aspekte Syntax, Semantik und Pragmatik. 1.4.1.1 Pragmatik Die Pragmatik einer Programmiersprache behandelt das Verhältnis der Zeichen einerseits zu Menschen, andererseits zu Rechnern: Welche Ideen drücken die Zeichen aus, wie verstehen und benutzen Menschen diese Zeichen? Was bewirken sie im Rechner? © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.4 Soll und Ist von Programmiersprachen 1 – 17 Bild 1.12 Pragmatik menschliche Pragmatik Programmiersprache mechanische Pragmatik Die menschliche Pragmatik untersucht Fragen wie Lesbarkeit, Verständlichkeit, Lehrbarkeit und Lernbarkeit von Programmiersprachen sowie ihre Anwendbarkeit und ihren Nutzen zur Lösung praktischer Aufgaben. Die mechanische Pragmatik untersucht Fragen wie die Übersetzbarkeit von Programmiersprachen, ihre Anforderungen an Betriebssysteme und Abhängigkeiten von Rechnerarchitekturen. Pragmatische Aspekte sind kaum formalisierbar, daher werden sie verbal beschrieben. Die Pragmatik ist Gegenstand der Diskussion, Entwicklung und Forschung. Sie ist eng verknüpft mit den Qualitätsmerkmalen, auf die 1.7 eingeht. 1.4.1.2 Semantik Die Semantik einer Programmiersprache behandelt die Bedeutung der Zeichen und ihre Beziehungen zu den Objekten, auf die sie anwendbar sind. Sie legt fest, was welches Sprachelement oder -konstrukt bedeutet und welche Wirkung es in einem Programmablauf hervorruft. Die Semantik wird beschrieben durch eine Menge von Verhaltensregeln, die die Funktionsweise von Programmen bestimmen. Man unterscheidet dynamische und statische Aspekte. Die dynamische Semantik befasst sich mit Bedeutungen von Sprachelementen zur Laufzeit von Programmen. Dazu gehören z.B. Antworten auf die Fragen: Wie werden Daten Speicherplätze zugeordnet? Wie werden Ausdrücke ausgewertet? Wie werden Anweisungen ausgeführt? Die statische Semantik befasst sich mit Bedeutungen von Sprachelementen zur Übersetzungszeit von Programmen – also ohne dass ein Programm laufen muss. Dazu gehören z.B. Kontextregeln (die die Syntax nicht ausdrückt), Sichtbarkeits- und Zugriffsregeln (Schachtelung, Rechte), Typregeln (Verträglichkeit, Anpassung). Bei manchen (meist älteren) Programmiersprachen ist die Semantik nicht formalisiert, sondern nur verbal beschrieben. Andere (meist jüngere) Programmiersprachen haben eine weitgehend formalisierte Semantik, denn dafür wurden theoretisch fundierte Methoden entwickelt. Drei wichtige Ansätze zur Formalisierung sind die operationale Semantik, die axiomatische Semantik, und die denotationale Semantik. 1.4.1.3 Syntax Die Syntax (im weiten Sinn) einer Programmiersprache behandelt Beziehungen der Zeichen untereinander, ihre Kombinierbarkeit ohne Rücksicht auf ihre spezielle Bedeutung und ihre Beziehung zur Umgebung. Sie legt fest, welche Sprachelemente und konstrukte es gibt und wie diese sich zusammensetzen. Die Syntax wird beschrieben durch Regeln, die die Struktur von Programmen bestimmen. Man unterscheidet zwei Ebenen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 18 1 Einführung Die Syntax (im engen Sinn) definiert die als Nichtterminale oder syntaktische Einheiten bezeichneten zusammengesetzten Arten bedeutungstragender Komponenten der Sprache, beschreibt ihre Strukturen durch Grammatiken oder Syntaxdiagramme, und legt allgemeine syntaktische Regeln fest. Die Lexik definiert die als Symbole oder lexikalische Einheiten bezeichneten elementaren Arten bedeutungstragender Komponenten der Sprache, beschreibt ihre Wertemengen durch reguläre Ausdrücke, und legt allgemeine lexikalische Regeln fest. Die Symbole einer Sprache sind ihre terminalen syntaktischen Einheiten und heißen daher auch kurz Terminale. Die Werte der Symbole heißen auch Token. Die Syntax der meisten Programmiersprachen ist weitgehend bis vollständig formalisiert. Notationen für syntaktische Regeln sind reguläre Ausdrücke, Syntaxdiagramme, und Grammatiken, vor allem in speziellen Ausprägungen als erweiterte Backus-Naur-Formen (EBNF).1, 2 Diese Notationen seien hier als bekannt vorausgesetzt. Die Grenze zwischen Syntax und Semantik ist unscharf. Es ist möglich, gewisse Eigenschaften einer Sprache alternativ als semantisch oder syntaktisch festzulegen. 1.4.2 Implementation von Programmiersprachen Die Implementation von Programmiersprachen teilt sich in die Aspekte Darstellung in der Zielsprache, Werkzeuge, und Implementationskonzepte. 1.4.2.1 Darstellung in der Zielsprache Die Darstellung der Quell- in der Zielsprache, insbesondere in der Maschinensprache (oder im Rechner) beantwortet die Fragen: Wie ist die Zielsprache definiert? Wie werden Literale der Quellsprache in der Zielsprache dargestellt? Gibt es dazu Standards, Normen? In welchen Formaten werden Zahlen gespeichert und verarbeitet? Mit welchen Zeichencodes werden Zeichen codiert? Wie werden andere Elemente der Quellsprache auf Elemente der Zielsprache, insbesondere auf Befehle der Maschinensprache, abgebildet? Dieser Aspekt lässt sich nicht eindeutig der Beschreibung oder der Implementation von Programmiersprachen zuordnen. So legt etwa die eine Sprache die Darstellung von Zahlen und Zeichen in ihrer Sprachbeschreibung fest, die andere überlässt dies ihren Sprachimplementationen. 1 John Warner Backus (geb. 1924), US-amerikanischer Informatik-Pionier, entwickelte für IBM die Programmiersprachen Speedcoding (1949) und Fortran (1954), schuf mit P. Naur die Backus-Naur-Form, arbeitete an den function-level-Programmiersprachen FP und FL. 2 Peter Naur (geb. 1928), dänischer Informatik-Pionier, schuf mit J. Backus die Backus-NaurForm, wirkte an der Entwicklung der Programmiersprache Algol 60 mit, kritisierte in seinem Buch Computing: A Human Activity (1992) die formalistische Schule des Programmierens. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.4 Soll und Ist von Programmiersprachen 1.4.2.2 1 – 19 Werkzeuge Die Werkzeuge (tool) zu einer Sprachimplementation umfassen die in Bild 1.11 genannten: Entwicklungsumgebung Texteditor zum Editieren des Quellprogrammtexts, Browser zum Stöbern in Bibliotheken und Erkunden von Schnittstellen von Modulen, Klassen und Komponenten, GUI-Builder zum Editieren des Layouts grafischer Benutzungsoberflächen und Generieren von Quelltextschablonen, Übersetzer (translator) zum Umformen des Quelltexts in die Zielsprache, Binder (linker) zum Binden der Objektcodeeinheiten getrennt übersetzter Quelltexteinheiten, Lader (loader) zum Laden des Maschinencodes in den Hauptspeicher, Laufzeitsystem (runtime system) als unmittelbare Umgebung des Maschinencodes, Debugger als Testhilfe für z.B. Einzelschrittablaufverfolgung (single stepping, tracing), Haltepunkte (breakpoint), Diagnoseausgabe bei erkannten Fehlern. Früher koexistierten diese Werkzeuge relativ unabhängig voneinander als kommandozeilenorientierte Programme. In den 1980er Jahren erschienen erste integrierte Entwicklungsumgebungen (integrated development environment, IDE) wie Turbo Pascal. IDEs sind Anwendungsprogramme, die wichtige Werkzeuge vereinen, um Softwareentwicklern die Arbeit zu erleichtern und sie von Routineaufgaben zu entlasten. Heute existieren zu den meisten Programmiersprachen mehr oder weniger mächtige visuelle IDEs, d.h. mit grafischen Benutzungsoberflächen. Moderne IDEs unterstützen auch Aspekte wie Quellprogrammverarbeitung: Generieren von Quelltextschablonen für Sprachkonstrukte, farbiges Hervorheben der Syntax, syntaxgesteuertes Editieren, halbautomatisches Ergänzen von Fragmenten, Formatieren und Analysieren von Quelltexten, Extrahieren von Schnittstellen, Spezifikationen, Dokumentationsteilen, Generieren von Diagrammen wie Struktogrammen und UML-Modellen aus Quelltexten; UML-Modellierung: grafische Editoren zum Erstellen von UML-Modellen und Generatoren, die aus UML-Modellen Quelltextschablonen erzeugen; Entwurfsmuster: Generieren von Quelltextschablonen für benötigte Klassen; Datenbankanbindung; Komponentenerzeugung; Versionsverwaltung; Projektmanagement. Eine IDE kann auch mehrere Programmiersprachen unterstützen, z.B. Eclipse. 1.4.2.3 Implementationskonzepte Übersetzen Interpretieren Kompilieren Binden Vorverarbeiten Kompilationsphasen 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 20 1.4.3 1 Einführung Vielfalt und Einheit von Programmiersprachen Entstehungsdaten Varianten Teilmengen: Auswahl von Elementen und Merkmalen der Originalsprache Erweiterungen: zusätzliche Elemente und Merkmale Dialekte: geänderte Elemente und Merkmale Familien Versionen: folgen zeitlich aufeinander Standards (herstellereigene Standards, Konsensstandards) Referenzmanual Sprachhandbuch 1.4.4 Umgebungen von Programmiersprachen Bibliotheken Rohbauten 1.5 Einflüsse auf und von Programmiersprachen Keine Programmiersprache entsteht aus nichts, keine existiert ewig. Wie alles, was Menschen schaffen, hat jede Programmiersprache ihre Vorgeschichte und Einflüsse, ihre Nutzungsdauer und Weiterentwicklung, und ihre Wirkungen. „Die richtigen Programmierkonzepte“ und „die richtige Programmiersprache“ kann es nicht geben, darauf hat schon C. A. R. Hoare hingewiesen. 1.5.1 Einflüsse auf Programmiersprachen Jeder Entwerfer einer Programmiersprache muss viele Aspekte berücksichtigen [PrZ98] S. 31: den intendierten Anwendungsbereich mit seinen Anforderungen, die zugrunde gelegten Plattformen – Rechnerarchitekturen und Betriebssysteme – mit ihrer Funktionalität (z.B. E/A-Modell: Stapel- oder Dialogverarbeitung oder GUI), die zu benutzenden Standards und Normen, das gewählte Programmierparadigma, den Entwicklungsstand der theoretischen Grundlagen der Informatik, Softwareentwicklungsmethodik (z.B. Spezifikationsmethode), Programmiersprachen mit den Methoden ihrer Beschreibung und Implementation (z.B. Kompiliertechniken), die vorhandenen Ressourcen (Arbeitskräfte, Zeit, Geld). 1.5.2 Einflüsse von Programmiersprachen Innovation Verbreitung © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.6 Entwicklung imperativer Sprachen 1.6 1 – 21 Entwicklung imperativer Sprachen Aus der Entwicklungsgeschichte der Programmiersprachen zeigt Bild 1.13 einen kleinen, hier relevanten Ausschnitt, der sich auf einige einflussreiche oder verbreitete imperative Sprachen beschränkt. Ein Pfeil mit durchgezogener Linie bedeutet eine Entwicklungslinie oder einen starken Einfluss, ein Pfeil mit einer gestrichelten Linie einen schwächeren Einfluss. Component Pascal Niklaus Wirth und Jürg Gutknecht starteten 1985 ein Projekt namens Oberon, aus dem das in der Oberon-Sprache geschriebene Oberon-System hervorging. Die Sprache Oberon ist eine Weiterentwicklung der von Wirth entworfenen Programmiersprachen Pascal und Modula-2; sie übernimmt von Pascal Grundkonzepte der strukturierten Programmierung, von Modula-2 Grundkonzepte der modularen Programmierung und bietet gegenüber ihren Vorgängersprachen zusätzlich objektorientierte Konzepte. Oberon-2 ist eine aufwärtskompatible Erweiterung von Oberon, von Wirth und Hanspeter Mössenböck 1991 entwickelt. Die Firma Oberon microsystems Inc. erweiterte Oberon-2 um komponentenorientierte Konzepte zu Component Pascal. Eiffel Eiffel wurde 1985 von Bertrand Meyer bei Interactive Software Engineering Inc. als neue objektorientierte Sprache entworfen, die erste Implementation lag 1986 vor. 1988 erschien Meyers Buch Object-Oriented Software Construction, in dem er den Sprachentwurf vorstellte [Mey90]. Nach einigen Änderungen wurden Eiffel und umfangreiche Klassenbibliotheken 1995 standardisiert. Es gibt mehrere Implementationen und Entwicklungsumgebungen auf den wichtigsten Plattformen. C C, dessen Spuren C++, Java, C#, D, Scala, Go sowie viele Skriptsprachen tragen, wurde um 1972 von Dennis Ritchie für eine Reimplementation des Betriebssystems Unix entworfen. Es war ursprünglich typlos, sollte eine in hardwarenaher Programmierung verwendete Assemblersprache ablösen, und Portierbarkeit von Systemprogrammen und Erzeugung effizienten Maschinencodes gewährleisten. C++ C++ wurde von Bjarne Stroustrup in den AT&T Bell Laboratories von 1980 bis 1983 als Erweiterung von C entwickelt, von 1983 bis 1986 veröffentlicht und in den Folgejahren mehrfach geändert und erweitert. Es gibt zahlreiche herstellerspezifische Dialekte von C++. Die Hauptentwicklungslinie stabilisierte sich Mitte der 1990er Jahre in einem ANSI- und ISO-Standard. Es gibt über 12 unabhängige Übersetzer, C++ steht auf praktisch allen Plattformen mit vielen Werkzeugen bereit. Java Java hieß ursprünglich Oak und war für den Einsatz in eingebetteten Systemen vorgesehen, bevor es auf Internet-Anwendungen orientiert wurde. Entwerfer von Java sind James Gosling, Bill Joy, Guy Steele und andere. Java wurde 1995 von Sun Microsystems mit gewaltigem, bisher bei Programmiersprachen unbekanntem Marketingaufwand als die Sprache für Web-Anwendungen, als besseres C++ und als Kampfansage an Microsoft veröffentlicht. C# C# ist Microsofts Antwort auf Java und jahrelange Rechtsstreitereien mit Sun. Die Entwerfer von C# sind Anders Hejlsberg, Scott Wiltamuth und Peter Golde. Hejlsberg war zuvor bei Borland Chef-Designer für Turbo Pascal und Delphi, deren Einfluss auf C# sichtbar ist. Die Sprachspezifikation und die erste Implementation wurden im Juli 2000 zusammen mit der Ankündigung der .NET-Plattform veröffentlicht. Die ECMA verabschiedete im Dezember 2002 einen C#-Standard [ECMA334]. D D wird seit 1999 von Walter Bright bei Digital Mars als besseres C++ für Systemprogrammierung entwickelt [D09]. D übernimmt von Oberon Module und von Eiffel Vertragskonstrukte. Scala Scala für „scalable language“ wird seit 2001 von Martin Odersky an der École Polytechnique Fédérale de Lausanne als Kombination objektorientierter und funktionaler Elemente entwickelt und seit 2011 vermarktet [Sca10]. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 22 1 Einführung Bild 1.13 Entwicklungslinien imperativer Programmiersprachen 1950 Assemblersprachen Fortran 1955 1960 Algol 60 Cobol PL/I 1965 BCPL Simula 67 Algol 68 1970 Pascal C 1975 1980 Smalltalk 80 Modula-2 Ada Turbo Pascal C++ 1985 Eiffel 1990 Object Pascal ANSI-C Oberon-2 Ada 95 Java 1995 Component Pascal C++ ISO 2000 D C# Delphi Scala Spec# 2005 2010 Go © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.7 Merkmale von Programmiersprachen 1 – 23 Spec# Spec# ist eine Erweiterung von C# zu einer Vertragsspezifikationssprache mit Eiffels Invarianten, Vor- und Nachbedingungen, die von Microsoft Research entwickelt wird und seit 2003 in die Entwicklungsumgebung Microsoft Visual Studio für die .NET-Plattform integriert ist [Spe12]. Go Go wurde von Robert Griesemer, Rob Pike und Ken Thompson bei Google Inc. als Sprache zur Programmierung verteilter Systeme entwickelt und 2009 veröffentlicht [Go10]. Das klassenlose Go kombiniert Javas Interfaces mit Oberons Verbunden und typgebundenen Prozeduren und übernimmt Konzepte nebenläufiger Programmierung von Hoares Communicating Sequential Processes (1978), bietet aber noch nicht einmal Zusicherungen. 1.7 Merkmale von Programmiersprachen 1.7.1 Nichttechnische Merkmale zu ergänzen 1.7.2 Technische Merkmale funktional: Paradigma, Abstraktionsebene, weitere Programmierkonzepte und Sprachkonstrukte Qualitätsmerkmale stellen sich selten zufällig ein. Deshalb formulieren Sprachentwerfer oft bestimmte Qualitätsmerkmale als Entwurfsziele. Siehe dazu [Lou93] S. 47ff. und [Woo96] S. 21ff. 1.7.3 Qualitätsmerkmale von Sprachbeschreibungen Problemlösungsmächtigkeit Ausdrucksstärke (expressiveness) Prägnanz (conciseness) Schreibbarkeit (writability) Lesbarkeit (readability) Verständlichkeit Benutzbarkeit Lernbarkeit Kompatibilität mit etablierten Konventionen und Notationen Zuverlässigkeit Sicherheit (safety) Speichersicherheit Typsicherheit Modulsicherheit Robustheit (robustness) Erkennen von Programmierfehlern Vermeiden von Unstetigkeiten Abschrecken vor Missinterpretationen und Missbrauch Wartbarkeit der Programme (maintainability) Umfang (size) Bestimmtheit (definiteness) und Genauigkeit (preciseness) der Sprachdefinition Wohldefiniertheit der syntaktischen und semantischen Beschreibungen 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 24 Fortran-Schleife 1.7.4 1 Einführung Ein Beispiel verdeutlicht, wie wichtig dieses Merkmal ist. Die erste Version von Fortran führte ein Konstrukt für eine Bedingungsschleife in die Programmierung ein, legte in der Sprachbeschreibung aber nicht fest, wann die Bedingung ausgewertet werden soll. In heutiger Terminologie: Es blieb offen, ob es sich um eine kopf- oder fußgesteuerte Bedingungsschleife handelt. Als Folge davon entschieden sich Sprachimplementierer für die eine oder andere Variante. Portierte Fortran-Programme produzierten Überraschungen. Dasselbe Programm lieferte auf verschiedenen Rechnern verschiedene Ergebnisse. Heute ist klar, dass unterschiedliche Schleifensemantiken den Effekt von Programmen beeinflussen. Vollständige formale Definition Beweisbarkeit der Korrektheit von Programmen Standardisierung Konzeptuelle Einfachheit (simplicity) Vermeiden unnötiger Komplexität Vermeiden unnötiger Redundanz, „eine gute Lösung für jedes Problem“ Einheitlichkeit, Regularität Vermeiden von Ausnahmen zu Regeln Vermeiden quantitativer Beschränkungen Konsistenz Ähnliche Konzepte ähnlich realisieren Unterschiedliche Konzepte unterschiedlich realisieren Allgemeinheit Orthogonalität (orthogonality): Grundkonzepte voneinander unabhängig und beliebig kombinierbar Abstraktionsgrad Portierbarkeit Maschinenunabhängigkeit Verknüpfbarkeit (interoperability) Schneller Zugriff auf Bibliotheken in anderen Sprachen Modularität Einschränkbarkeit auf Teilsprachen Erweiterbarkeit um weitere Sprachkonzepte Effiziente Übersetzbarkeit von Quellprogrammen Effiziente Implementierbarkeit der Sprachkonstrukte Qualitätsmerkmale von Sprachimplementationen Effiziente Übersetzung von Quellprogrammen Schnelle Übersetzung von Quellprogrammen Qualität des erzeugten Objektcodes Zuverlässige Implementation der Sprachkonstrukte Effiziente Implementation der Sprachkonstrukte Niedrige Entwicklungskosten Systematische, schnelle Implementierbarkeit Portierbarkeit Adaptierbarkeit © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.8 Überblick über die Auswahlsprachen 1.8 1 – 25 Überblick über die Auswahlsprachen Zunächst seien die Auswahlsprachen Component Pascal, Eiffel, C++, Java, C#. anhand der Klassifikationskriterien von 1.1 geordnet. Tabelle 1.2 Klassifikation der Auswahlsprachen Klassifikationskriterium Component Pascal Abstraktionsebene Eiffel C++ Java C# höhere Programmiersprache Mehrzweck Anwendungsbereich Lehre, Systemprogrammierung Rolle im Softwareentwicklungsprozess Implementation Systemprogrammierung Spezifikation + Implementation Programmierparadigma Implementation imperativ Notation textuell Ausführbarkeit maschinell ausführbar Ausführendes Organ Implementationskonzept 1.8.1 Web Prozessor virtuelle Maschine Kompilation Kompilation + Interpretation Ziele der Sprachentwürfe Sprachen werden zu bestimmten Zwecken entworfen. Welche Ziele standen den Entwerfern der Auswahlsprachen vor Augen? Component Pascal Oberon entstand wegen Schwächen von Modula-2, es übernimmt dessen Stärken wie strenge Typbindung und Unterstützung für strukturiertes, prozedurales und modulares Programmieren. Neues Hauptziel ist die Unterstützung erweiterbaren Programmierens, erreicht durch Typerweiterung (Vererbung). Außerdem soll Oberon wirklich maschinenunabhängig, einfacher und einheitlicher als Modula-2 sein. Bei Oberon wurde das Prinzip des ökonomischen Designs, d.h. die Beschränkung auf Wesentliches angewandt. Die Erweiterungen von Component Pascal beziehen sich auf das Programmieren von Systemen mit dynamisch ladbaren Komponenten. Eiffel Der Entwurf von Eiffel ist softwaretechnisch begründet: Eiffel soll sich für komplexe Softwaresysteme mit hohen Qualitätsanforderungen eignen. Als einfache, rein objektorientierte Sprache mit strengem Typkonzept, Zusicherungen, disziplinierter Ausnahmebehandlung, Generizität usw. soll es die Entwicklung von zuverlässiger, korrekter, robuster, fehlertoleranter, erweiterbarer, wiederverwendbarer, portierbarer, effizienter Software ermöglichen. Außerdem soll es die Reversierbarkeit des Entwicklungsprozesses unterstützen. C++ Stroustrup beschreibt folgende Entwurfsziele für C++ [Str97], [Str94a]. „A general purpose language designed to make programming more enjoyable for the serious programmer.“ C++ soll „a better C“ sein. Kompatibilität mit C: Alle C-Programme sollen als C++ kompilierbar und mit CBibliotheken bindbar sein. Es wird nicht versucht, Probleme durch Entfernen von 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 26 Java 1 Einführung Spracheigenschaften zu lösen, z.B. durch Säubern der Syntax. Spätere Einschränkung: So nahe an C wie möglich, aber nicht näher. Gründe dafür: C ist weit verbreitet, flexibel, effizient, portierbar. Gegenüber diesen Vorteilen sind Nachteile von C wie die Sicherheitslücken und die seltsame Syntax von Vereinbarungen nachrangig. „Nahe an der Maschine ...“: hardwarenahe Mechanismen, Effizienz, Vermeidung von Laufzeitaufwand; Einsetzbarkeit in traditionellen Kompilierer-, Binder- und Laufzeitsystemen; Portierbarkeit. „... und nahe am zu lösenden Problem“: Abstraktionsmechanismen, Datenabstraktion und objektorientierte Programmierung; Typsystem mit statischer Typprüfung; Werkzeug für Bibliotheken programmiererdefinierter Typen hoher Qualität, die handlich, sicher und effizient zu benutzen sind; Vorrang für programmiererdefinierte Typen, die aber wie sprachdefinierte Typen benutzbar sind. C++ ist nur eine Sprache innerhalb eines Systems, mit Schnittstellen zu anderen Sprachen und Systemkomponenten. Einfachheit der Sprache wird auch als Entwurfsziel genannt. Leicht genug lernbar, um Benutzer anzusprechen. Leicht genug implementierbar, um Implementierer anzusprechen, d.h. mit Algorithmen nicht komplizierter als lineares Suchen. Sun beschreibt Java in einem frühen Papier so [Sal98] S. 778: „A simple, object-oriented, distributed, interpreted, robust, secure, architecture neutral, portable, high-performance, multithreaded, and dynamic language.“ Gosling, Joy und Steele schreiben [GJS96]: C# „Java is a general-purpose, concurrent, class-based, object-oriented programming language, specifically designed to have as few implementation dependencies as possible. Java allows application developers to write a program once and then be able to run it everywhere on the Internet.“ Wiltamuth und Hejlsberg schreiben [WiH04]: „C# (pronounced "C sharp") is a simple, modern, object-oriented, and type-safe programming language. It will immediately be familiar to C and C++ programmers. C# combines the high productivity of Rapid Application Development (RAD) languages and the raw power of C++.“ Der Vorspann des ECMA-Standards nennt zudem [ECMA334]: Unterstützung für softwaretechnische Prinzipien wie strenge Typprüfungen, Prüfungen von Reihungsindexgrenzen, Entdecken von Zugriffen auf uninitialisierte Variablen, automatische Speicherbereinigung, Robustheit, Haltbarkeit, Produktivität. Entwicklung von Softwarekomponenten für verteilte Umgebungen. Portierbarkeit von Quellcode und Programmierern, besonders von C/C++-Programmierern. Unterstützung für Internationalisierung. Eignung für ein breites Anwendungsspektrum auf Plattformen vom eingebetteten System zum Großrechner, vom kleinen zum großen Betriebssystem. Effizienz; C# soll dabei aber nicht mit C oder Assembler konkurrieren. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.8 Überblick über die Auswahlsprachen 1.8.2 1 – 27 Quantitative Eigenschaften Lassen sich Programmiersprachen messen? Kaum – nur einzelne Merkmale lassen sich in Zahlen ausdrücken. Tabelle 1.3 Quantitative Eigenschaften Eigenschaft Component Pascal Eiffel C++ Fassung: Urfassung: 156 Langf.: 594 annotiert: 570 Kurzf.: 100 Standard: > 700 Java C# > 505 471 Umfang des Sprachreferenzmanuals in Seiten 29 – 34 Syntax: Anzahl der Nichtterminale (ohne lexikalische Elemente) 34 154 125 81 271 spezielle Wörter, daruntera 76 63 74 52 78 reservierte Wörter, darunter 40 63 73 48 77 8 2 2 9 2 1 9 1 1 15 3 1 1 4 1 1 3 1 1 sprachdefinierte Typen Werte Größen Schlüsselwörter, darunter sprachdefinierte Typen Werte Größen Prozeduren 36 12 3 21 spezielle Symbole 27 27 46 46 45 Operatorsymbole 19 25 54 37 44 OperatorPrioritätsstufen 4 12 18 14 14 a Die Begriffe „reserviertes Wort“ und „Schlüsselwort“ werden in der Literatur nicht einheitlich gebraucht. Hier gilt: Ein reserviertes Wort kann nicht als Name benutzt werden. Ein Schlüsselwort ist nur in speziellen Kontexten ein spezielles Wort. Aufgabe 1.1 Zahlen interpretieren 27.9.12 Welche Eigenschaften der Sprachen sind Tabelle 1.3 zu entnehmen? © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 28 1.8.3 1 Einführung Grundstrukturen von Programmen Hier seien die syntaktischen Strukturen der Auswahlsprachen betrachtet. Component Pascal Component Pascal ist eine modulare Sprache: Ein Component-Pascal-Quellprogramm besteht aus einer Menge von Modulen. MODULE Modulname; Importabschnitt Vereinbarungen von Konstanten, Typen, Variablen, Prozeduren BEGIN Initialisierungen CLOSE Finalisierungen END Modulname. Mindestens eines der Module eines Programms muss mindestens eine Prozedur exportieren, bei der die Ausführung des Programms beginnen kann. Ein Programm kann beliebig viele Einstiegsprozeduren haben. Ein Aspekt der Komponentenorientierung von Component Pascal ist, dass die getrennt kompilierten Module bei Bedarf dynamisch gebunden und geladen werden. Eiffel Eiffel ist eine rein objektorientierte Sprache: Ein Eiffel-Quellprogramm besteht aus einer Menge von Klassen. class Klassenname deklarative Abschnitte feature Vereinbarungen von Merkmalen (Abfragen, Kommandos) invariant Klasseninvarianten end Zur Ausführung eines Eiffel-Programms, in Eiffel-Terminologie eines Systems, gehört die Angabe einer Wurzelklasse und einer Erzeugungsprozedur, zur Laufzeit existiert eine dynamische Menge von Objekten. Ein Programm kann beliebig viele Einstiegsprozeduren haben. Klassen werden getrennt kompiliert und statisch gebunden. C++ C++ ist als Erweiterung von C zunächst eine prozedurale (funktionsorientierte, aber nicht funktionale!) Sprache: Ein C++-Programm besteht als Quelltext aus Vereinbarungen von Daten und Funktionen. Zusätzlich enthält es Direktiven für den von C übernommenen Vorübersetzer (preprocessor), auf die nicht ganz verzichtet werden kann. Vorübersetzerdirektiven Vereinbarungen von Konstanten, Typen, Variablen, Funktionen int main () { Vereinbarungen Anweisungen return ganzzahliger Ausdruck; } Es gibt nur eine einzige Einstiegsfunktion. Sie heißt stets main, kann Parameter besitzen und liefert stets einen ganzzahligen Ergebniswert an die aufrufende Umgebung. Programmteile werden unabhängig voneinander kompiliert und statisch gebunden. Eine von C geprägte syntaktische Eigenheit ist das Blocken mit geschweiften Klammern. Da C++, Java, C# und andere Sprachen das übernommen haben, heißen sie auch {}-Sprachen. Java Java ist eine rein objektorientierte Sprache: Ein Quellprogramm besteht aus einer Menge von Klassen und Schnittstellen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.8 Überblick über die Auswahlsprachen 1 – 29 class Klassenname { Vereinbarungen von Feldern, Methoden, geschachtelten Klassen und Schnittstellen, Initialisierern, Konstruktoren } interface Schnittstellenname { Vereinbarungen von Konstanten, abstrakten Methoden, geschachtelten Klassen und Schnittstellen } Zur Ausführung eines Programms gehört die Angabe einer Wurzelklasse, die eine Methode namens main enthalten muss, bei der der Ablauf startet. Klassen und Schnittstellen werden getrennt kompiliert. Klassen werden dynamisch gebunden und geladen. C# C# ist eine rein objektorientierte Sprache, die ihre Abstammung von Java nicht verleugnen kann: Ein Quellprogramm besteht aus einer Menge von Klassen und Schnittstellen. class Klassenname { Vereinbarungen von Daten (Konstanten, Felder), Funktionen (Methoden, Eigenschaften, Ereignisse, Indexer, Operatoren, Konstruktoren, Destruktoren), geschachtelten Typen } interface Schnittstellenname { Vereinbarungen von Methoden, Eigenschaften, Ereignissen, Indexern } In C# gehört wie in Java zur Ausführung eines Programms die Angabe einer Wurzelklasse, die eine Methode namens Main enthalten muss, bei der der Ablauf startet. Klassen und Schnittstellen werden getrennt kompiliert. Klassen können dynamisch gebunden und geladen werden. Als erstes Beispiel zeigt Tabelle 1.4 primitivst mögliche Programme. Tabelle 1.4 Minimale Programme Component Pascal MODULE Nothing; END Nothing. Eiffel class NOTHING end C++ int main () {} Java C# class Nothing { } class Nothing { } 1.8.4 Unterstützte Programmierstile, Abstraktionsebenen, Sprachkonzepte Programmierstil Ein Aspekt ist, wie weit eine Sprache bestimmte Programmierstile ermöglicht oder unterstützt. Eine Sprache ermöglicht einen Programmierstil, wenn sie Sprachkonstrukte bereitstellt, die unter geeigneten Richtlinien eingesetzt das Programmieren in diesem Stil erlauben. Eine Sprache unterstützt einen Programmierstil, wenn sie Sprachkonstrukte für diesen Stil bereitstellt, zum Programmieren in diesem Stil ermutigt und es gleichzeitig erschwert oder verhindert, auf eine Weise zu programmieren, die diesem Stil widerspricht. Grenzen sind freilich fließend. Tabelle 1.5 bezieht sich auf die Programmierstile von 1.2.1 und verwendet diese Symbole: 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 30 1 Einführung Programmierstil nicht unterstützt, nicht möglich. Programmierstil nicht unterstützt, aber eingeschränkt möglich mit Nachteilen. ☺ Programmierstil unterstützt. Tabelle 1.5 Unterstützte Programmierstile Component Pascal Eiffel Strukturiert ☺ ☺ Prozedural ☺ Modular ☺ ADS ☺ ADT ☺ ☺ Objektorientiert ☺ ☺ Komponentenorientiert ☺ Programmierstil Generisch Nebenläufig Bibliotheken C++ Java C# ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ ☺ Bibliotheken ☺ ☺ Betrachtet man die Programmierstile prozedural, modular und objektorientiert als Alternativen, so gilt: Component Pascal und C++ sind hybride Multistilsprachen: Component Pascal unterstützt prozedurales, modulares und objektorientiertes Programmieren. C++ unterstützt prozedurales und objektorientiertes Programmieren und ermöglicht modulares Programmieren. Eiffel, Java und C# sind eher Monostilsprachen, sie unterstützen objektorientiertes Programmieren. Component Pascal deckt alle Stile bis auf generisches und nebenläufiges Programmieren ab. Generisches Programmieren mit Eiffel und C* zeigen die Kapitel 3 und 4. Zum Kennenlernen nebenläufigen Programmierens konzentriert sich der zweite Teil der Vorlesung Informatik 3 auf Java. Abstraktionsebene Sprachmerkmal Ein zweiter Aspekt ist, welche Abstraktionsebenen eine Sprache erreicht. Tabelle 1.6 bezieht sich auf Abstraktionsebenen von 1.3 und verwendet diese Symbole: Abstraktionsebene verdeckt. Abstraktionsebene nachteilig erreicht oder schwach unterstützt. ☺ Abstraktionsebene gut erreicht. Zum dritten Aspekt, in welchen besonderen Merkmalen sich die Sprachen unterscheiden, verwendet Tabelle 1.7 diese Symbole: Nützliches Sprachmerkmal leider nicht vorhanden, ggf. eingeschränkt simulierbar. Nützliches Sprachmerkmal nicht vorhanden, aber näherungsweise simulierbar. ☺ Nützliches Sprachmerkmal vorhanden. Ohne Smiley: Sprachmerkmal beschränkt nützlich oder fragwürdig. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.8 Überblick über die Auswahlsprachen 1 – 31 Tabelle 1.6 Erreichte Abstraktionsebenen Abstraktionsebene Maschinennähe Typisierung, Strukturierung von Ausdrücken, Anweisungen, Daten, Abstraktion von Algorithmen, Daten, Verhalten Component Pascal Eiffel C++ ☺ SYSTEM- unsicher, Modul goto ☺ ☺ Bibliotheken Java ☺ unsafeModifikator, goto ☺ ☺ ☺ ☺ ☺ ☺ ☺ Delegate, Objektorientierte Entwurfsmuster Rohbauten C# Iterator ☺ BlackBox ☺ .NET Tabelle 1.7 Unterscheidende Sprachmerkmale Sprachmerkmal Vertragsspezifikation, Prädikatenlogik Namensräume Component Pascal Eiffel Zusicherung Modul, Subsystem Automatische Speicherbereinigung Ausnahmebehandlung Getrennte Übersetzung Prozeduren Metaprogrammierung 27.9.12 ☺ ☺ silent trap Modul Zusicherung Zusicherung nein nein ☺ Meta- Zusicherung ☺ nein ☺ C# ☺ Aufzählungstypen Überladung Java ☺ Mehrfaches Erben Schachtelung ☺ C++ ☺ ☺ ☺ package ☺ interface interface Klassen Klassen Klassen ☺ ☺ ☺ Funktionen, Operatoren Methoden Methoden ☺ ☺ ☺ ☺ ☺ ☺ ☺ reflection, ☺ lambda ☺ Vorübersetzer ☺ templates annotations expressions, method attrib. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 32 1.8.5 1 Einführung Sprachdefinitionen und Syntaxbeschreibungen zu ergänzen 1.8.6 Sprachimplementationen Component Pascal Obwohl der Umfang von Component Pascal relativ klein ist, enthält es genügend Konstrukte, um Programme für einen weiten Anwendungsbereich zu schreiben. Ein Satz von Standardprozeduren ist Bestandteil der Sprachdefinition. Ein-/Ausgabe ist nur mit Standardbibliotheken möglich. Der BlackBox Component Builder der schweizer Firma Oberon microsystems Inc. ist eine integrierte Entwicklungsumgebung mit Browser, Editor, Kompilierer, Interpretierer und Lader. BlackBox soll die Entwicklung wiederverwendbarer Komponenten unterstützen. Es besteht aus einer Bibliothek (library) von Grundkomponenten, einem Rohbau (framework) namens BlackBox Component Framework, d.h. einer erweiterbaren Ansammlung wiederverwendbarer Schnittstellen, und einer Menge direkt benutzbarer Standardkomponenten, die diesen Rohbau implementieren oder erweitern. Entwickler können BlackBox durch zusätzliche Module in existierenden Subsystemen oder durch zusätzliche Subsysteme erweitern. Eiffel Nur mit Sprachkonstrukten von Eiffel kann man kein interaktionsfähiges Programm erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Es gibt Werkzeuge und Bibliotheken für Eiffel von mehreren Herstellern. Bertrand Meyers amerikanische Firma Eiffel Software Inc. (früher Interactive Software Engineering, ISE) bietet mit EiffelStudio eine integrierte Multiplattform-Entwicklungsumgebung in freien und kommerziellen Varianten, und mit EiffelEnvision ein Plug-In für Visual Studio .NET. Die deutsche Firma Object Tools GmbH liefert mit Visual Eiffel eine integrierte Windows-Intel-Entwicklungsumgebung in freien und kommerziellen Varianten. SmartEiffel (früher SmallEiffel), ein GNU-Open-Source-Projekt des InformatikForschungszentrums LORIA in Nancy, umfasst Kompilierer, Debugger, Bibliotheken und verschiedene Werkzeuge für Windows, Linux und Unix. Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von EiffelStudio zu benutzen. C++ C++ enthält viele Operatoren, aber nur mit Sprachkonstrukten kann man kein arbeitsfähiges Programm schreiben. Die Sprache entfaltet ihre Stärken zusammen mit den zugehörigen Vorübersetzerdirektiven und umfangreichen Klassenbibliotheken. Ein-/ Ausgabe ist nur mit Standardbibliotheken möglich. Es gibt unzählige Werkzeuge und Bibliotheken für C++. Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von Code::Blocks zu benutzen. Java Nur mit Sprachkonstrukten von Java kann man kein interaktionsfähiges Programm erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Werkzeuge und Bibliotheken für Java gibt es von mehreren Herstellern, vor allem aber von Sun Microsystems, seit 2010 Oracle Corporation. C# Nur mit Sprachkonstrukten von C# kann man kein interaktionsfähiges Programm erstellen, man braucht einen Komplex von Standardklassenbibliotheken dazu. Werkzeuge und Bibliotheken für C# gibt es vor allem von Microsoft, insbesondere die integrierte Entwicklungsumgebung Visual Studio .NET. Für das Informatik 3 Praktikum empfiehlt sich, die freie Windows-Variante von SharpDevelop zu benutzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 1.8 Überblick über die Auswahlsprachen 1.8.7 1 – 33 Zielsprachen und Übersetzungskonzepte Höhere Programmiersprachen werden als Quellsprachen in Zielsprachen übersetzt. Konzepte des Übersetzens sind Kompilation und Interpretation, die sich kombinieren lassen. Zielsprache und Übersetzungskonzept sind keine Eigenschaften der Quellsprache, sondern Eigenschaften des Übersetzers oder der Entwicklungsumgebung. Eine Quellsprache lässt sich zumindest prinzipiell mit verschiedenen Übersetzungskonzepten in relativ beliebige Zielsprachen übersetzen. Das Folgende geht auf die Übersetzungskonzepte ein, die einige Übersetzer und Entwicklungsumgebungen der fünf betrachteten Sprachen einsetzen. Entwurfsaspekte Component Pascal Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen: In welche Zielsprache soll übersetzt werden? Soll kompiliert oder interpretiert werden? Der BlackBox-Übersetzer kompiliert Component Pascal direkt in Maschinensprache (native code). Der Kompilierer ist schnell; er übersetzt die rund 240 Module von BlackBox selbst in wenigen Augenblicken. Die Sprache ist so entworfen, dass ein SinglePass-Compiler mit einfachem Top-Down-Parser mit rekursivem Abstieg sie übersetzen kann. Der erzeugte Maschinencode ist hinsichtlich Effizienz mit laufzeitoptimiertem C-Code vergleichbar. Es gibt Kompilierer, die Component Pascal in Java-Bytecode übersetzen. Qualität von Kompilierern Exkurs. Viele Kompilierer verwenden „Optimierungen“, um effizienteren Code zu erzeugen. Solche Optimierungen erhöhen aber die Komplexität und den Umfang eines Kompilierers und verlängern die Kompilationszeiten. Niklaus Wirth verwendete eine intuitive Metrik, um ein günstiges Verhältnis zwischen der Effizienz des Kompilierers und der Effizienz des erzeugten Codes zu finden [Fra00]. Der Kompilierer ist in der Quellsprache geschrieben, die er übersetzt. (Man erreicht dies durch ein Bootstrap-Verfahren.) Das Maß für die Qualität des Kompilierers ist seine Selbstkompilationszeit: die Zeit, die er braucht, um sich selbst zu übersetzen. Eine Optimierung wird nur in den Kompilierer aufgenommen, wenn sie seine Selbstkompilationszeit reduziert, sonst lohnt sich der Aufwand nicht. Wegen dieser Vorgehensweise haben Wirths Kompilierer keine separaten Optimierungsphasen. Man kann mit derselben Metrik den Nutzen mancher Sprachkonzepte prüfen, da ein Kompilierer ein hinreichend komplexes Programm ist. Führt der Einsatz des Sprachkonstrukts im Kompilierer nicht zu einer kürzeren Selbstkompilationszeit, so ist sein Nutzen fraglich. Diese Metrik führt gleichzeitig zu einfachen Sprachentwürfen, schnellen Kompilierern und effizientem Objektcode. Eiffel EiffelStudio von Eiffel Software kompiliert zweistufig: Erst wird Eiffel in C, dann C mit einem gängigen C-Kompilierer in Assembler- oder Maschinensprache übersetzt. Dieses Konzept ist inzwischen auch bei anderen Sprachen verbreitete Praxis. Der Vorteil ist die Portierbarkeit des Eiffel-Systems: Da C auf vielen Plattformen bereitsteht, ist Eiffel auf diese Plattformen portierbar. EiffelStudio verwendet C also als maschinenunabhängigen Luxusassembler, um Portierbarkeit zu erzielen. Ein Nachteil dabei ist langsame Übersetzung, die sich besonders bei großen Programmsystemen mit vielen Nachkompilationen negativ auf die Produktivität auswirkt. Um diesem Nachteil entgegenzuwirken, entwickelte Eiffel Software die so genannte Melting-Technik. Dabei werden kleine Änderungen an Eiffel-Quellcode mit schon erzeugtem C-Code verschmolzen und interpretiert, ohne vollständig in Maschinencode übersetzt zu werden. Der vom Eiffel-Kompilierer erzeugte C-Code ist für Menschen kaum lesbar und nicht für manuelle Änderungen gedacht. EiffelEnvision von Eiffel Software kompiliert in die Common Intermediate Language (CIL) der .NET-Umgebung. Visual Eiffel übersetzt direkt in die Maschinensprache der Intel-X86-Prozessoren. SmartEiffel übersetzt wahlweise in C oder Java-Bytecode. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 1 – 34 1 Einführung C++ Als C++ noch C with Classes hieß, entwickelte Stroustrup für seine Spracherweiterungen den Vorübersetzer cfront, der sie in C übersetzte. Der Vorübersetzer wuchs und verbreitete sich mit C++. Später wurden unabhängig voneinander mehrere Kompilierer entwickelt, die C++ in Assembler- oder Maschinensprache übersetzen. Java Die Übersetzung von Java erfolgt zweistufig. Dem Slogan „write once, run everywhere“ entsprechend wird Java üblicherweise in eine abstrakte Maschinensprache, den Bytecode kompiliert, der dann durch eine Java Virtual Machine (JVM) interpretiert wird. Eine JVM kann in einen Web-Browser integriert sein, um Java-Applets zu interpretieren. An einem Applet beteiligte Klassen werden dynamisch über das Internet geladen. Der Vorteil der Maschinenunabhängigkeit ist klar. Ein Nachteil ist, dass Interpretation mindestens eine Größenordnung langsamer als Kompilation ist. Um diesem Nachteil entgegenzuwirken, verwendet man heute auch Just-In-Time-Compiler (JIT), die den Bytecode unmittelbar vor der Ausführung kompilieren. Inzwischen werden auch andere Sprachen in Java-Bytecode kompiliert. Historisches Exkurs. Die Ideen des Bytecodes und der virtuellen Maschine sind nicht neu. Niklaus Wirth benutzte schon 1966 im Kompilierer zu seiner ersten Sprache Euler einen Interpretierer als Back-End. Um Portierungen von Pascal zu erleichtern, lieferte Wirth Anfang der 1970er Jahre eine freie Variante des Pascal-Kompilierers, der einen abstrakten Maschinencode namens PCode erzeugte. Eine virtuelle Maschine namens Pascal-P interpretierte den P-Code. Pascal-PImplementationen standen bald auf vielen Rechnern bereit. Der Java-Bytecode ähnelt dem PCode, die JVM dem Pascal-P-Interpretierer. C# Auch die Übersetzung von C# erfolgt zweistufig. Ähnlich wie Java wird C# in einen abstrakten Maschinencode kompiliert, der firmenintern Microsoft Intermediate Language (MSIL) und im ECMA-Standard Common Intermediate Language (CIL) heißt. Für die Übersetzung von MSIL stehen drei verschiedene JIT-Compiler zur Verfügung. Im Unterschied zu Java, das eine Sprache für alle Plattformen sein soll, fokussiert Microsoft weniger auf Maschinenunabhängigkeit als auf Interoperabilität vieler Sprachen auf einer gemeinsamen Plattform: Ada, APL, Beta, C, C++, C#, Cobol, Component Pascal, Eiffel, F#, Forth, Fortran, Haskell, Java, JavaScript, Lisp, Logo, Modula2, Oberon, Pascal, Perl, PHP, Prolog, Python, Ruby, Scala, Scheme, Smalltalk, Spec#, Standard MetaLanguage, Tcl/Tk, Visual Basic.NET und viele andere Sprachen werden auf .NET alle in MSIL kompiliert und lassen sich so beliebig kombinieren.1 1 Brian Ritchie’s .NET Development Site: dotnetpowered Language List. http://dotnetpowered.com/languages.aspx; .NET Languages: Resources .NET Language Sites. http://www.dotnetlanguages.net/DNL/Resources.aspx (Zugriffe 2012-03-08). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2 Einmal quer durch die Sprachen Aufgabe Beispiel 2 Bild 2 2 wird (2) endgen Leitlinienimmermehr, Programm 2 2 O, dieser Streit Tabelle 2 Stets wird die Wahrheit hadern mit dem Schönen, Stets wird geschieden sein der Menschheit Heer In zwei Partein: Barbaren und Hellenen. Heinrich Heine (1797 – 1856) Es träumte mir von einer Sommernacht Dieses Kapitel führt mit kleinen Beispielprogrammen in die Auswahlsprachen Component Pascal, Eiffel, C++, Java und C# ein. Formuliert in strukturiertem, modularem und objektorientiertem Programmierstil, demonstrieren sie Standardlösungen zu den Standardproblemen sequenzielle Textausgabe, Argumentübernahme von der Kommandozeile, Konvertierung von Zeichenketten in Zahlen, Vereinbarungen von Klassen und Objekten. Wichtige Sprachkonstrukte werden angewandt, aber nicht jedes Detail wird diskutiert. 2.1 Hello-World-Programme Problem Wie sagt uns ein laufendes Programm, dass es läuft? Schon die seit Kernighans und Ritchies C-Buch ([KeR88], [KeR90]) unvermeidlichen zeilenorientierten Hello-WorldProgramme nutzen viele Sprachmerkmale und spiegeln typische Sprachideen. Einer einfachen Lösung folgt meist eine raffiniertere Variante mit den Aspekten Dokumentation, geschützte Konstante, öffentliche Routine (Oberbegriff für Prozedur und Funktion). 2.1.1 Component Pascal Component Pascal bietet eine modulare Lösung. Der Quelltext des Moduls I0HelloWorld wird in der Datei I0/Mod/HelloWorld.odc gespeichert. Programm 2.1 Component Pascal: Hello World MODULE I0HelloWorld; IMPORT Out; PROCEDURE Do*; BEGIN Out.Open; Out.String ("Hello World"); Out.Ln; END Do; END I0HelloWorld. Zur Ausgabe importiert I0HelloWorld das nur für Lehrzwecke vorgesehene Standardbibliotheksmodul Out. Die Prozedur Do gehört wegen der Exportmarke „*“ zur Schnittstelle von I0HelloWorld. Der Aufruf des Kommandos I0HelloWorld.Do führt dazu, dass die Module Out und dann I0HelloWorld dynamisch gebunden und geladen werden. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 2 – Seite 1 von 42 2–2 2.1.2 2 Einmal quer durch die Sprachen Eiffel Eiffel löst das Problem objektorientiert. Der Quelltext der Klasse HELLO_WORLD_1 wird in der Datei HELLO_WORLD_1.e gespeichert. (Auf Unix Klein-Großschreibung beachten – auch bei anderen Sprachen!) Programm 2.2 Eiffel: Hello World 1 class HELLO_WORLD_1 create make feature make do print ("Hello World%N") end end Der create-Abschnitt deklariert die Prozedur make zu einer Erzeugungsprozedur (creation procedure). Erzeugungsprozeduren heißen konventionell oft make. Nur Erzeugungsprozeduren mit speziellen Signaturen können als Wurzelprozeduren für Programmabläufe dienen. feature startet einen Abschnitt mit öffentlichen Merkmalen, wovon es hier nur make gibt. HELLO_WORLD_1 ist wie jede Klasse (außer ANY und GENERAL) implizit Nachfolger der Klasse ANY, die von GENERAL erbt, wo die von make aufgerufene Ausgabeprozedur print definiert ist, d.h. print gehört durch Vererbung automatisch zu HELLO_WORLD_1. %N erzeugt eine neue Zeile. Programm 2.3 Eiffel: Hello World 2 note description: "Simple example for standard output" author: "Mrs. Anybody" history: "07-03-24, 11-10-06 syntax updated" class HELLO_WORLD_2 create {NONE} make feature {NONE} Message : STRING = "Hello World" make -- Print current object with a simple message in a line. do print (Current) io.put_string (" says: " + Message) io.put_new_line end -- make end -- class HELLO_WORLD_2 Dokumentation In der raffinierten Variante dient der note-Abschnitt (früher: indexing) der Beschreibung der Klasse, vergleichbar dem meta-Etikett (tag) von HTML. Dokumentationswerkzeuge können note-Abschnitte, Verträge und spezielle Kommentare wie die Beschreibung von make extrahieren. Das Folgende dokumentiert die Eiffel-Beispiele im Eiffel-Stil, um professionelle, softwaretechnisch sinnvolle Dokumentation zu zeigen. Der create-Abschnitt beschränkt die Erzeugungserlaubnis auf die Menge {NONE}, also die Klasse NONE, deren Schnittstelle leer ist und deren Implementation die Erzeugungserlaubnis nicht nutzt. So kann es von HELLO_WORLD_2 keine Objekte außer dem Wurzelobjekt geben. Da der Abschnitt der Merkmalsvereinbarungen durch © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.1 Hello-World-Programme Exportbeschränkung 2–3 feature {NONE} eingeleitet ist, werden die Merkmale make und Message nur an NONE exportiert. So sind make und Message vor Kundenzugriffen geschützt. Dies zeigt exemplarisch das Konzept des auf eine spezifizierte Menge von Klassen beschränkten Merkmalsexports. Als Beispiel für eine Konstantenvereinbarung in Eiffel ist für die interessante Nachricht das konstante Attribut Message durch Konstante Message : STRING = "Hello World" vereinbart. STRING ist eine Standardbibliotheksklasse. Eine Konstante hat in jedem Objekt der vereinbarenden Klasse denselben Wert, muss also nicht in jedem Objekt einen eigenen Speicherplatz belegen, sondern nur einen für alle Objekte der Klasse. Eiffel überlässt es Compilerbauern, Konstanten speichereffizient zu implementieren. Was ist in make anders? „--“ startet einen Zeilenendkommentar. print hat die Signatur Polymorphie print (some : GENERAL) mit some als polymorphem Parameter, wodurch print alle Arten von Objekten ausgeben kann, indem es some.out ausgibt, da jedes Objekt mit der von ANY geerbten Abfrage out : STRING eine redefinierbare Standarddarstellung von sich liefert. Entsprechend gibt print (Current) das mit dem Standardnamen Current bezeichnete aktuelle Objekt aus, also die Zeichenkette Current.out. make nutzt auch die von ANY geerbte Abfrage io des Typs STD_FILES, die Zugriffe auf die Standarddateien Eingabe, Ausgabe und Fehler ermöglicht. „+“ ist zwischen Zeichenkettenoperanden der Konkatenationsoperator. Das verbose io.put_new_line vermeidet das kryptische %N. 2.1.3 C++ C++ bietet eine hybride Lösung. Der Quelltext wird in einer beliebig benannten Datei mit der konventionellen Namenserweiterung cpp gespeichert; hier lautet der Dateiname HelloWorld1.cpp. Programm 2.4 C++: Hello World 1 #include <iostream> int main () { std::cout << "Hello World\n"; } Vorübersetzer Um Ausgabe zu ermöglichen, muss die Schnittstellendatei (header file) iostream.h für Ein-/Ausgabe-Ströme mit der Vorübersetzerdirektive #include inkludiert werden. Dadurch wird vor dem Kompilieren der Inhalt der Datei textuell für die Inkludierungsdirektive eingesetzt. Der davon unabhängig zur Implementationsdatei iostream.cpp erzeugte Objektcode muss zum kompilierten main dazugebunden werden. Die in iostream.h vereinbarten Größen gehören zum Namensraum (namespace) std. Auf diese Größen wird mit qualifizierten Namen, hier std::cout, zugegriffen; „::“ ist der Sichtbarkeitsbereichsauflösungsoperator (scope resolution operator). Namen von Dateien (iostream) und Namensräumen (std) sind unabhängig voneinander frei wählbar. cout bezeichnet ein globales Wertobjekt des Typs ostream, den Standardausgabestrom. (Wo dieses Objekt vereinbart ist und wie es initialisiert wird, bleibt im Dunkeln.) „<<“ ist ein Operator der Klasse ostream, der hier auf cout angewandt wird. (Die Notation erinnert an die Eingabeumlenkung der Unix-Shell.) Die Zeichenkette ist Parameter des Operators. \n erzeugt eine neue Zeile. Die Aufrufstelle erhält von main als Ergebnis 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2–4 2 Einmal quer durch die Sprachen implizit 0 zurück. Der Rückgabewert ist bedeutungslos, offenbar ist die Hauptsache an der main-Funktion der erzeugte Nebeneffekt. Programm 2.5 C++: Hello World 2 #include <iostream> int main (int argc, char * argv[]) { std::cout << "Hello World" << std::endl; } Nebeneffektbehafteter Ausdruck Leitlinie 2.1 Vermeide Nebeneffekte Diese Variante zeigt die optionale Signatur von main, um Argumente von der Aufrufstelle zu übernehmen; darauf geht 2.2 ein. Die wichtige Lektion ist, dass der von C geerbte algorithmische Kern von C++ auf dem Konzept nebeneffektbehafteter Ausdrücke beruht. Dazu ist das kryptische \n durch std::endl ersetzt. Mathematische Ausdrücke liefern Werte, bewirken aber keine Nebeneffekte. Dagegen ist ein Ausdruck als Anweisung einer imperativen Programmiersprache nur sinnvoll, wenn er einen Nebeneffekt bewirkt und der gelieferte Wert überflüssig ist. Dieses Konzept ist einerseits ausdrucksstark, andererseits wegen der Nebeneffekte schwer verständlich, fehlerträchtig und daher softwaretechnisch problematisch. Nebeneffekte behindern systematisches Entwickeln zuverlässiger Software stark. Da die C*-Sprachen die Konzepte der Ausdrucksanweisung und der nebeneffektbehafteten Ausdrücke und Operatoren enthalten, kann man mit ihnen zwar nicht nebeneffektfrei programmieren, sich aber auf sprachbedingte Nebeneffekte beschränken: Vermeide Nebeneffekte, wo sie sich nicht durch Sprachkonstrukte umgehen oder softwaretechnisch gut begründen lassen! Die Ausdrucksanweisung (expression statement) Ausdrucksanweisung std::cout << "Hello World" << std::endl; besteht aus fünf Ausdrücken, von denen zwei Nebeneffekte bewirken. Um das zu erkennen, schreiben wir die Anweisung um in die äquivalente Punktnotation, wobei wir die Qualifikation std:: vereinfachend weglassen. cout ist ein nebeneffektfreier Ausdruck, der ein Objekt bezeichnet. Der Operator << ist an cout gebunden und mit einem Punkt selektierbar; seine Parameter setzen wir in runde Klammern: (cout.operator<<("Hello World")).operator<<(endl); Der aktuelle Parameter "Hello World" ist ein konstanter, nebeneffektfreier Ausdruck. Der Ausdruck cout.operator<<("Hello World") gibt als Nebeneffekt "Hello World" aus und liefert als Wert eine Referenz auf das Objekt namens cout. Durch die folgende Selektion mit „.“ wird die Referenz implizit dereferenziert. Auf das referenzierte Objekt cout wird wieder << angewandt, diesmal mit dem nebeneffektfreien Ausdruck endl als Parameter. Der Ausdruck cout.operator<<(endl) bewirkt als Nebeneffekt eine neue Ausgabezeile und liefert als Wert eine Referenz auf cout. Da der Ausdruck als Anweisung mit „;“ abgeschlossen ist, wird der Wert nicht benötigt und weggeworfen. Historisches Exkurs. Die Idee der Ausdrucksanweisung stammt von Niklaus Wirth, der sie 1966 mit seiner ersten Programmiersprache Euler veröffentlichte [WiW66a], [WiW66b]. Während Wirth dieses Konzept bald verwarf und schon in seinen nächsten Sprachen Algol-W und Pascal die saubere Trennung zwischen Ausdrücken und Anweisungen bevorzugte, fand die Ausdrucksanweisung über Algol 68 Eingang in C, von dem aus sie sich fortpflanzte [Lin96] S. 42. Algol 68 machte sogar aus jeder Anweisung einen Ausdruck! Sprachen, die die Konzepte Anweisung und Ausdruck vermischen, nennt man ausdrucksorientiert. Dazu schreibt Wirth [Wir05]: © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.1 Hello-World-Programme 2–5 „The heart of the trouble lies in the elimination of the fundamental distinction between statement and expression. The former is an instruction, and it is executed; the latter stands for a value to be computed, and it is evaluated.“ Überladener Operator Zweitens beruht die Ausdrucksanweisung auf dem in C++ beliebten Überladen (overloading) von Operatoren. Bei Aufrufen überladener Funktionen und Operatoren wählt der Kompilierer die passenden Implementationen statisch anhand der Typen der aktuellen Parameter. Von << werden hier zwei Varianten aufgerufen: Die erste Variante hat einen formalen Parameter des Typs „konstanter Zeiger auf Zeichen“, die zweite einen des Typs „Zeiger auf eine Funktion, die einen Parameter des Typs Referenz auf basic_ostream hat und deren Rückgabetyp auch Referenz auf basic_ostream ist“. Der aktuelle Parameter "Hello World" ist eine Zeichenkette und passt zur ersten Variante. Der aktuelle Parameter endl – ein Manipulator – ist eine Funktion mit zur zweiten Variante passender Signatur. Die zum Aufruf cout.operator<<(endl) gewählte Implementation von << ruft die übergebene Funktion endl so auf: endl(cout) Dieser Aufruf in pseudofunktionalem Stil führt zum Aufruf cout.endl() der Methode endl(), die zu ostream gehört und hier am Objekt cout angewandt wird. Die Kombination verschiedener Konzepte erlaubt es also, kurz cout << "Hello World" << endl; statt etwa cout.print("Hello World"); cout.endl(); oder cout.printline("Hello World"); zu schreiben. Das einfache Beispiel zeigt, wie die Ausdrucksstärke von C++ die Schreibbarkeit (nicht die Problemlösungsmächtigkeit) erhöht, aber auch zu schwer durchschaubarem Quelltext führt. Historisches Exkurs. Das Überladen von Operatoren ist keine Innovation von C++, sondern aus Algol 68 und Ada 83 bekannt. Da die C++-Welt viele, aber keine einheitlichen Dokumentationsregeln kennt, verzichtet dieser Text auf eine dokumentierte Variante. Um nach den prozeduralen Varianten etwas Objektorientierung ins Spiel zu bringen, folgen eine modulare und eine objektorientierte Variante. Jede definiert eine Klasse, die von der unvermeidlichen mainFunktion benutzt wird. In Programm 2.6 fungiert die Klasse als Modul, sie hat kein Datenelement, eine expandierte Klassenfunktion und wird nicht getrennt übersetzt. In Programm 2.7 hat die Klasse ein konstantes Datenelement, eine nicht expandierte Elementfunktion, wird getrennt übersetzt und durch ein lokales Wertobjekt benutzt. Die verschiedenen Aspekte lassen sich beliebig mit weiteren Aspekten kombinieren. public Programm 2.6 enthält die Definition der Klasse HelloWorld, die als einziges Element die Funktion act besitzt (so genannt, weil do ein reserviertes Wort ist). Da act in einem public-Abschnitt definiert ist, gehört act zur Schnittstelle der Klasse. static Der Spezifikator static macht act zu einer Klassenfunktion, d.h. einer Funktion, die an die Klasse, nicht an einzelne Objekte gebunden ist. Das Schlüsselwort static weist hier nicht auf etwas Statisches hin, sondern wurde vom Sprachentwerfer gewählt, damit er kein weiteres Schlüsselwort einführen musste. Klassen, die nur mit static spezifizierte Elemente enthalten, entsprechen Singletons oder Modulen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2–6 Programm 2.6 C++: Hello World 3 mit Klasse 2 Einmal quer durch die Sprachen #include <iostream> class HelloWorld { public: static void act () { std::cout << "Hello World" << std::endl; } }; int main () { HelloWorld::act(); } void void bezeichnet einen wertelosen Nichtstyp, der hier als Ergebnistyp von act angegeben ist, weil act nichts zurückgibt, also eigentlich keine Funktion, sondern eine Prozedur ist. Die C*-Welt kennt nur Funktionen, Prozeduren sind als void-Funktionen zu definieren. Das leere Klammernpaar () zeigt an, dass die Formalparameterliste von act leer ist. In den C*-Sprachen sind void und () von C geerbter syntaktischer Ballast, da act statt void act () genügen würde, um das Notwendige auszudrücken. Historisches Exkurs. C hat void von Algol 68 übernommen, das zudem die Schlüsselwörter empty, nil, skip und den Begriff undefined kennt, um die Abwesenheit von etwas auszudrücken. „Too much of nothing makes a man feel ill“, kommentiert Bob Dylan. Inlining Zwischen den geschweiften Klammern steht der Anweisungsteil von act. Da er innerhalb der Klassendefinition steht, handelt es sich um eine Inline-Funktion, d.h. der Kompilierer kann den Anweisungsteil expandieren, d.h. an Aufrufstellen einsetzen und so die Routinensprungbefehle sparen. Als zu einer void-Funktion gehörend braucht der Anweisungsteil von act keine return-Anweisung zu enthalten. Im Anweisungsteil von main wird das act von HelloWorld aufgerufen. Da act eine Klassenfunktion ist, wird der Aufruf mit dem Klassennamen und dem Sichtbarkeitsbereichsauflösungsoperator :: qualifiziert. Die Klasse wird wie ein Modul verwendet. Die Auswertung des void-Ausdrucks HelloWorld::act() bewirkt den gewünschten Effekt als Nebeneffekt und liefert nichts zurück. Wiederverwendbarkeit Klassen sollen wiederverwendbar sein. Damit eine Klasse ohne Replikation wiederverwendbar ist, muss sie getrennt übersetzbar sein. Doch C/C++ kennen nur unabhängiges Übersetzen; getrenntes Übersetzen simulieren sie mit einem Vorübersetzer auf dem Technikstand der frühen 1970er Jahre. Entsprechend muss der C++-Programmierer den Quelltext einer Klasse in einen Schnittstellen- und (mindestens) einen Implementationsteil trennen und auf (mindestens) zwei Dateien verteilen. Für main ist eine dritte Datei vorzusehen. Bild 2.1 Inkludierungsbeziehungen zu Programm 2.7 HelloWorld.hpp HelloWorld4.cpp Inkludierung string.h iostream.h HelloWorld.cpp Bild 2.1 zeigt, dass in diesem einfachen Beispiel keine Schnittstellendatei mehrfach in eine Implementationsdatei inkludiert wird. Bei realen Anwendungen passiert es aber leicht, dass Schnittstellendateien indirekt über Inkludierungsketten mehrfach inkludiert werden. Die mehrfach vorkommenden Bezeichner führen dann beim Kompilieren zu Kontextfehlern mit der Meldung „duplicate symbol“. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.1 Hello-World-Programme Programm 2.7 C++: Hello World 4 mit wiederverwendbarer Klasse 2–7 // Header file: HelloWorld.hpp #ifndef HELLO_WORLD_HPP_ #define HELLO_WORLD_HPP_ #include <string> class HelloWorld { protected: static const std::string message; public: void act (); }; const std::string message = std::string("Hello World"); #endif // HELLO_WORLD_HPP_ // Implementation file: HelloWorld.cpp #include "HelloWorld.hpp" #include <iostream> void HelloWorld::act () { std::cout << message << std::endl; } // Implementation file: HelloWorld4.cpp #include "HelloWorld.hpp" int main () { HelloWorld hello; hello.act(); } Um das Problem zu vermeiden, gewöhnt sich der professionelle Programmierer an die Konvention, die Inhalte von Schnittstellendateien mit ifndef-define-endif-Vorübersetzerdirektiven zu klammern. Dazu ist zu jeder Schnittstellendatei ein systemweit eindeutiges Symbol zu wählen, das z.B. konventionell aus ihrem Namen so abgeleitet ist: Alle Buchstaben groß, zwischen Teilwörtern, für den Punkt und am Ende ein Unterstrich. Die Direktive #ifndef X_HPP_ prüft, ob das Symbol X_HPP_ definiert ist; falls nicht, wird der folgende Text bis zur Direktive #endif inkludiert, sonst nicht. Falls also inkludiert wird, definiert die Direktive #define X_HPP_ das zuvor undefinierte Symbol, was ein zweites Inkludieren derselben Datei verhindert. Leitlinie 2.2 C++: Inkludiere nur bedingt Klammere den C++-Quelltext in der Schnittstellendatei x.hpp mit den Vorübersetzerdirektiven #ifndef X_HPP_ #define X_HPP_ C++-Quelltext #endif // X_HPP_ um mehrfaches Inkludieren zu vermeiden! 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2–8 Schnittstellendatei Exportbeschränkung Konstante 2 Einmal quer durch die Sprachen Die Schnittstellendatei HelloWorld.hpp enthält die Definition der Klasse HelloWorld. Dazu gehört nicht nur die Kundenschnittstelle der Klasse, sondern auch ein Teil ihrer Implementation, nämlich hier das durch protected geschützte Datenelement message. Wegen protected dürfen nur von HelloWorld abgeleitete Klassen auf message zugreifen. message ist in der Klassendefinition von HelloWorld als Datenelement des Typs std::string, einer in string.h definierten Standardbibliotheksklasse, deklariert durch Deklaration static const std::string message; Der Spezifikator const macht message zu einem konstanten Datenelement. Der Spezifikator static macht message zu einem Klassendatenelement, damit es nicht in jedem Objekt eigenen Speicherplatz belegt. const ist nur kombiniert mit static sinnvoll, doch ist die Kombination nicht zwingend; der Programmierer ist also gefordert, diesen Hinweis auf eine effiziente Implementation hinzuschreiben. Das Beispiel zeigt, wie Orthogonalität mit Effizienz konfligieren kann. Leitlinie 2.3 C++: Vermeide replizierte Konstanten Konstante Datenelemente in Klassen sollen Klassendatenelemente sein. Verwende also in Klassendefinitionen const bei Datenelementen nur mit static! message muss außerhalb der Klasse in einer Definition initialisiert werden, wofür C++ die drei äquivalenten Notationen Definition const std::string message = std::string("Hello World"); const std::string message ("Hello World"); const std::string message = "Hello World"; bietet, die zu einem Aufruf eines passenden Konstruktors der string-Klasse führen, der message mit der interessanten Nachricht initialisiert. Implementationsdatei Die Implementationsdateien HelloWorld.cpp und HelloWorld4.cpp sind unabhängig voneinander zu übersetzen, die erzeugten Objektdateien sind danach zusammenzubinden. Entwicklungsumgebungen kombinieren Übersetzungs- und Bindeschritte, fordern dazu aber meist die Definition eines Projekts zum Verwalten der Übersetzungseinheiten. HelloWorld.cpp enthält die Implementation der Elementfunktion act der Klasse HelloWorld. Um den Bezug zu ihrer Klasse herzustellen, wird ihr Name mit dem Klassennamen und :: qualifiziert. act benutzt message als Argument für den Ausgabeoperator <<, von dem der Kompilierer die geeignet überladene Variante wählt. HelloWorld4.cpp enthält die main-Funktion, die ein lokales Wertobjekt namens hello der Klasse HelloWorld vereinbart. Daraus ist zu erfahren, dass in C* lokale Vereinbarungen nicht syntaktisch vom Anweisungsteil getrennt, sondern Teil desselben sind. 2.1.4 Java Java bietet eine objektorientierte Lösung, die syntaktisch C++ ähnelt. Der Quelltext der Klasse HelloWorld1 wird in der Datei HelloWorld1.java gespeichert. Die erzeugte Bytecode-Datei heißt HelloWorld1.class. Programm 2.8 Java: Hello World 1 public class HelloWorld1 { public static void main (String[] args) { System.out.println("Hello World"); } } Beim Starten der HelloWorld1-Klasse als Anwendung wird die main-Methode ausgeführt. main ist wegen des Modifikators public öffentlich und wegen static eine Klassenmethode, d.h. sie gehört zur Klasse, nicht zu den Objekten. Ihr Ergebnistyp ist void, weil sie nichts zurückgibt. Wie bei C++ übernimmt main Argumente von der © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.1 Hello-World-Programme 2–9 Aufrufstelle, hier aber obligatorisch. String ist eine Standardbibliotheksklasse, [] markiert eine offene Reihung. Die Ausgabemethode println gehört zum Objekt out, dessen Klasse PrintStream ist. out ist eine Klassenvariable der Klasse System. Von System können keine Objekte erzeugt werden, diese Klasse existiert nur in der Rolle eines Singletons oder Moduls. Java vermeidet die trickreiche Ausgabe von C++ zugunsten einer klaren prozeduralen, nebeneffekt- und überladungsfreien Lösung wie in Component Pascal und Eiffel. Programm 2.9 Java: Hello World 2 /** Simple example for standard output. * @author Mrs. Anybody * @version 04-09-09 */ public class HelloWorld2 { private static final String MESSAGE = "Hello World"; /** * Prints a simple message in a line. */ public static void main (String[] args) { System.out.println(MESSAGE); } } Dokumentation Die zweite Variante, Programm 2.9, benutzt dem note-Abschnitt von Eiffel entsprechende Javadoc-Etikette zur Beschreibung der Klasse. Javadoc ist ein Werkzeug, das speziell etikettierte Kommentare in Dokumentationsdateien extrahieren kann. Professionelle Programmierer sollten alle Java-Programme mit Javadoc-Etiketten nach Konventionen dokumentieren. Es unterbleibt im Folgenden nur, um Platz zu sparen und weil die Lesbarkeit von Javadoc-Kommentaren zu wünschen übrig lässt. Für die interessante Nachricht ist das konstante Feld MESSAGE durch Private Konstante private static final String MESSAGE = "Hello World"; vereinbart – ein Beispiel für eine Konstantenvereinbarung in Java. Durch den Modifikator private ist MESSAGE nur in HelloWorld2 selbst zugreifbar, also vor Zugriffen von Kundenklassen (aber auch Erweiterungsklassen) geschützt. Der Modifikator final macht MESSAGE zusammen mit dem Initialisierungswert "Hello World" konstant. Der Modifikator static macht MESSAGE zu einer Klassenvariablen, damit es nicht in jedem Objekt eigenen Speicherplatz belegt. final ist nur kombiniert mit static sinnvoll, doch ist die Kombination nicht zwingend; wie bei C++ muss der Programmierer diesen Implementationshinweis hinschreiben. Leitlinie 2.4 Java: Vermeide replizierte Konstanten Applet Konstante Felder sollen Klassenfelder sein. Verwende also final bei Feldern nur mit static! Java führte die Idee ein, kleine Programme in Web-Seiten zu integrieren. Solche Programme heißen Applets; es handelt sich um Klassen, die die Standardbibliotheksklasse java.applet.Applet erweitern. Sie werden auf Web-Servern gespeichert und von Web-Browsern dynamisch geladen und ausgeführt (direkt oder über Java-Plug-Ins). Die Beispiel-Applet-Klasse HelloWorld3 (Programm 2.10) soll den Gruß in ein buntes Rechteck zeichnen. Durch die extends-Klausel erbt sie alle Merkmale von java.applet.Applet. Grafik 27.9.12 Die Größe des farbigen Rechtecks legen die Konstanten WIDTH und HEIGHT des Typs int fest. Die öffentliche Methode paint ist leer von java.applet.Applet geerbt. paint wird vom Web-Browser implizit aufgerufen. Für einen auf der Web-Seite sichtbaren Effekt ist paint zu überschreiben. Der Parameter g ist von der Klasse © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 10 2 Einmal quer durch die Sprachen java.awt.Graphics, die Grafikmethoden bietet. Im Einzelnen legt paint hier die Hintergrundfarbe java.awt.Color.green fest, füllt damit das links oben justierte Rechteck der Größe WIDTH * HEIGHT aus, legt die Schriftfarbe java.awt.Color.red fest, und zeichnet den Text "Hello World" in das Rechteck, beginnend in der Mitte. Programm 2.10 Java: Hello World 3 public class HelloWorld3 extends java.applet.Applet { private static final int WIDTH = 300, HEIGHT = 200; public void paint (java.awt.Graphics g) { g.setColor(java.awt.Color.green); g.fillRect(0, 0, WIDTH, HEIGHT); g.setColor(java.awt.Color.red); g.drawString("Hello World", WIDTH / 2, HEIGHT / 2); } } Das Applet wird in einem HTML-Text (Programm 2.11) durch das applet-Etikett spezifiziert, wobei ihm ähnlich wie einem Bild ein Rechteck zur Ausgabe zugeordnet wird (das größer als das im Applet spezifizierte farbige Rechteck sein sollte). Programm 2.11 HTML: Applet-Aufruf HelloWorld3 <html> <head> <title>Simple Java-Applet</title> </head> <body> Vor dem Applet. <p align="center"> <applet code="HelloWorld3.class" alt="HelloWorld3-Applet" width="600" height="300"> Hier stehen Parameter für das Applet. </applet> <p> Nach dem Applet. </body> </html> 2.1.5 C# C# bietet eine objektorientierte Lösung, die sich nur geringfügig von der Java-Variante unterscheidet. Der Quelltext der Klasse HelloWorld1 wird in der Datei HelloWorld1.cs gespeichert. Programm 2.12 C#: Hello World 1 public class HelloWorld1 { public static void Main () { System.Console.WriteLine("Hello World"); } } Die Main-Methode darf parameterlos sein, kann aber auch wie bei Java Argumente von der Aufrufstelle übernehmen. System ist nicht wie in Java eine Klasse, sondern ein Namensraum. Die Klasse oder das Singleton-Objekt System.Console enthält die Klassenmethode WriteLine, die eine Zeichenkette auf die Konsole ausgibt. Dokumentation Die zweite Variante benutzt dem note-Abschnitt von Eiffel entsprechende empfohlene XML-Abschnitte zur Beschreibung der Klasse. In Visual Studio .NET erzeugt ein Werkzeug daraus eine XML-Datei mit Dokumentationskommentaren. Analog zu Java sollten Profi-Programmierer alle C#-Programme mit XML-Abschnitten nach Konventionen dokumentieren. Ich ignoriere die Regel, weil ich mit Platz geize, XML-Abschnitte noch schlechter als Javadoc-Kommentare lesen kann und nicht einsehe, weshalb ich Zeit mit Tippen von XML-Etiketten verbringen sollte. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.1 Hello-World-Programme Programm 2.13 C#: Hello World 2 2 – 11 /// <summary> /// Simple example for standard output. /// </summary> /// <remarks> /// author: Mrs. Anybody; /// history: 04-09-09 /// </remarks> public class HelloWorld2 { protected const string Message = "Hello World"; /// <summary> /// Prints a simple message in a line. /// </summary> public static void Main (string[] args) { System.Console.WriteLine(Message); } } Für die interessante Nachricht ist die Konstante Message durch Geschützte Konstante protected const string Message = "Hello World"; vereinbart – ein Beispiel für eine Konstantenvereinbarung in C#. Der Modifikator protected exportiert Message (wie in C++, aber anders als in Java!) nur an Erweiterungsklassen von HelloWorld2, schützt es also vor Zugriffen von Kundenklassen. string ist ein Aliasname für die Standardbibliotheksklasse System.String. Der Modifikator const markiert Konstanten. Da an Objekte gebundene Konstanten Speicherplatz verschwenden, bindet C# Konstanten stets an die Klasse; der Modifikator static ist bei Konstanten weder nötig noch erlaubt. Die Entwerfer von C# haben sich hier also für Effizienz, gegen Orthogonalität entschieden. 2.1.6 Weitere Sprachen Neben und nach C# hat sich die C*-Familie weiter verzweigt. Auf die Hello-WorldProgramme der drei Sprößlinge D, Scala und Go werfen wir einen Blick. Programm 2.14 D: Hello World import std.stdio; void main(string[] args) { writefln("Hello world"); } std.stdio bezeichnet das Modul stdio im Paket std. Der Programmablauf beginnt bei main. Der Ergebnistyp von main kann auch int sein, seine Argumente können ent- fallen. Noch Fragen? Siehe [D09]. Programm 2.15 Scala: Hello World 1 object HelloWorld1 { def main(args: Array[String]) { println("Hello world") } } Das Schlüsselwort object macht HelloWorld1 zu einem Einzelobjekt (singleton) oder Modul. Da es die Methode main enthält, ist es ein Objekt der obersten Ebene (top-level object). Der Programmablauf beginnt bei main. Programm 2.16 Scala: Hello World 2 object HelloWorld2 extends Application { println("Hello world") } Es geht auch ohne main: Ist ein Einzelobjekt mit extends Application vereinbart, so werden alle Anweisungen in diesem Objekt ausgeführt. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 12 Programm 2.17 Scala: Hello World 3 2 Einmal quer durch die Sprachen println("Hello world") Es geht auch ohne object: Steht nur die Anweisung in der Datei HelloWorld3.scala, so ruft die Kommandozeile scala HelloWorld3.scala nicht den Scala-Kompilierer, sondern den Scala-Interpretierer auf, der die Anweisung in HelloWorld3.scala interpretiert. Es geht auch ohne Datei: Die Kommandozeile scala -e "println(\"Hello world\")" bewirkt dasselbe. Die Idee, eine Teilsprache einer Hochsprache als interpretierte Kommandosprache zu verwenden, stammt von Niklaus Wirths Oberon-System [ReW94]; siehe dazu auch 2.2.1.1. Mehr zu Scala in [Sca10]. Programm 2.18 Go: Hello World package main import "fmt" func main() { fmt.Printf("Hello World\n") } Der Quelltext gehört zum Paket main. Das Paket fmt wird importiert. Der Programmablauf beginnt bei der Funktion main. Weitere Fragen beantwortet [Go10]. 2.1.7 Fazit Die Hello-World-Programme lassen folgende Sprachideen erkennen: Einfach modular löst Component Pascal die Aufgabe. Rein objektorientiert mit Vererbung löst Eiffel die Aufgabe, wobei ein polymorpher Parameter das zentrale objektorientierte Konzept der Polymorphie nutzt. Hybride prozedural-modular-objektorientierte Lösungen erscheinen in C++, wobei nebeneffektbehaftete Operatoren unvermeidlich und das nicht objektorientierte Konzept des Überladens von Operatoren und Funktionen beliebt ist. Hybrid modular-objektorientiert lösen Java und C# die Aufgabe, wobei sie sich syntaktisch an C++ orientieren, aber semantisch auf Nebeneffekte verzichten. Java bietet mit Applets die interessante Variante, Java-Programme in Web-Browsern auszuführen. Die Ausgabe erfolgt textuell formatiert in ein Dokument, das direkt speicherbar und weiterverarbeitbar ist (Component Pascal/BlackBox), textuell unformatiert auf die Kommandokonsole in flüchtiger Form, die Speicherung und Weiterverarbeitung nur indirekt relativ aufwändig zulässt (Eiffel, C*), grafisch in ein Rechteck in einer Web-Seite (Java-Applet). In allen Sprachen lassen sich Hello-World-Varianten formulieren, die statt sequenzieller Textausgabe Elemente grafischer Benutzungsoberflächen nutzen. Aufgabe 2.1 Hello World Lernen Sie mit den Hello-World-Programmen die Entwicklungsumgebungen kennen! Erweitern Sie Ihre Lösungen spielerisch um Elemente Ihrer Wahl! Suchen Sie im Web und in Lehrbüchern nach Hello-World-GUI-Varianten! Sprachen: Alle. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2.2 2 – 13 Argumentübernahme von der Kommandozeile Beim Aufruf eines Programms Argumente von der Aufrufstelle an das Programm übergeben – dieses Konzept geht auf C und die Unix-Shell zurück. Traditionell wird ein Programm durch ein Kommando der Kommandosprache des Betriebssystems – kurz: von der Kommandozeile – aufgerufen: Kommandozeile Problem command options argument argument ... Der Kommandospracheninterpretierer übergibt die Argumente an das aufgerufene Programm. C++ hat das Sprachkonstrukt für die Übernahme der Kommandoargumente von C direkt übernommen. Andere Sprachen setzen das Konzept ähnlich um. Da moderne Anwendungen generell grafische Benutzungsoberflächen bieten, haben Kommandoargumente und sequenzielle Textausgabe an Bedeutung verloren. Trotzdem betrachten wir die Realisierungen dieser Konzepte in den Auswahlsprachen, weil sie bei Beispiel- und Übungsprogrammen oft nützlich sind. Argumente übergeben ist meist leichter als Eingabeanweisungen oder Dialoge programmieren. Die Argumente werden als Zeichenketten übergeben. Handelt es sich um Zahlen, so sind diese programmseitig in Zahlenformate zu konvertieren. Das Folgende untersucht die Mittel und Programmierschemas, mit denen die Auswahlsprachen die Konversion durchführen. 2.2.1 Component Pascal 2.2.1.1 Argumentübernahme Der BlackBox Component Builder umfasst einen Rohbau erweiterbarer Komponenten. Allgemein ist der Zweck eines Rohbaus, ihn zu erweitern. Speziell BlackBox wird erweitert, indem man einzelne Module oder Komponenten aus mehreren Modulen hinzufügt. Die Möglichkeit, mit BlackBox Stand-Alone-Programme zu entwickeln, die mit dem Binder zu ausführbaren Programmen (auf MS-Windows: EXE-Dateien) zusammengebunden werden, bleibt deshalb hier außer Acht. Stattdessen betrachten wir, wie gewöhnliche Prozeduren beliebiger Module innerhalb von BlackBox von beliebigen Stellen – z.B. Dokumenten, Menüs, Querverweisen, Dialogen – aus aufgerufen werden können. An die Stelle des Kommandointerpretierers des Betriebssystems tritt der Kommandointerpretierer von BlackBox. Er akzeptiert Prozeduren mit beliebig vielen Zeichenketten- und Ganzzahlargumenten in beliebiger Reihenfolge. Programm 2.19 Component Pascal: Parameterübernahme MODULE I0ParameterWriter; IMPORT StdLog; PROCEDURE Do* (IN s1, s2 : ARRAY OF CHAR; i1, i2 : INTEGER); BEGIN StdLog.Open; StdLog.String ("s1: " + s1); StdLog.Ln; StdLog.String ("s2: " + s2); StdLog.Ln; StdLog.String ("i1: "); StdLog.Int (i1); StdLog.Ln; StdLog.String ("i2: "); StdLog.Int (i2); StdLog.Ln; END Do; END I0ParameterWriter. Programm 2.19 übernimmt vier Argumente vom Kommandoaufruf und gibt sie aus. Ein Aufruf der Prozedur Do kann beispielsweise so in einem Dokument stehen: ! "I0ParameterWriter.Do ('erste Zeichenkette', 'zweite Zeichenkette', 1, 2)" 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 14 2.2.1.2 2 Einmal quer durch die Sprachen Konversion Soll die Prozedur Gleitpunktzahlen erhalten, so sind diese als Zeichenketten einzugeben und prozedurseitig in Gleitpunktzahlen zu konvertieren; dazu ist z.B. die Zeile StdLog.String ("s1: " + s1); StdLog.Ln; durch VAR r result : REAL; : INTEGER; Strings.StringToReal (s1, r, result); IF result = 0 THEN StdLog.String ("s1 as real: "); StdLog.Real (r); StdLog.Ln; END; zu ergänzen oder zu ersetzen. Die Lösung folgt dem prozeduralen Ein-/AusgabeSchema: StringToReal ist eine Prozedur mit einer Zeichenkette als Eingabeparameter und der konvertierten Zahl als Ausgabeparameter. Was passiert, wenn die Zeichenkette keine Gleitpunktzahl enthält, sich also nicht sinnvoll konvertieren lässt? Dieser Fall sollte nicht ignoriert, sondern angezeigt werden. Die Lösung ist ein Ergebnisparameter: Ein zweiter Ausgabeparameter zeigt Erfolg oder Misserfolg bei der Konversion an. Die Behandlung des Misserfolgs kann ggf. entfallen. Ein-/Ausgabe-Schema mit Ergebnisparameter (* Try to produce target from any source: *) Module.Compute (source, target, result); (* Check result to see if target is usable: *) IF result is good THEN process target ELSE handle bad case END; 2.2.2 Eiffel 2.2.2.1 Argumentübernahme Die Eiffel-Implementation von Eiffel Software bietet einen Mechanismus zur Argumentübernahme von der Kommandozeile, der sich am C/C++-Mechanismus orientiert.1 Andere Eiffel-Implementationen realisieren die Argumentübernahme ähnlich. Die Erzeugungsprozedur einer Wurzelklasse darf einen Parameter des Typs ARRAY [STRING] haben: make (arguments : ARRAY [STRING]) Generische Klasse ARRAY ist eine generische Standardbibliotheksklasse für Reihungen. STRING ist hier als aktueller generischer Parameter für den Elementtyp der Reihung eingesetzt. Programm 2.20 übernimmt beliebig viele Argumente von der Kommandozeile und gibt sie aus. Beim Aufruf von ARGUMENTS_WRITER.make werden die Kommandoargumente an das Zeichenkettenreihungsobjekt übergeben, auf das sich die Referenz arguments bezieht. Referenzsemantik Ohne aktuelle Argumente braucht es kein Reihungsobjekt zu geben! Wegen der Referenzsemantik ist zu unterscheiden, ob sich arguments auf nichts (Void) oder ein Objekt bezieht. Void ist ein von ANY geerbtes Attribut des Typs NONE, von dem es keine Objekte geben kann; deshalb stellt Void den leeren Bezug dar. Nur falls arguments /= Void ist die Selektion eines Objektmerkmals wie arguments.lower legal, wobei arguments implizit dereferenziert wird. 1 http://docs.eiffel.com/ (Zugriff 2005-03-21). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile Programm 2.20 Eiffel: Argumentübernahme an der Wurzel 2 – 15 note description: "Output of command arguments in the root class creation procedure" class ARGUMENTS_WRITER create make feature make (arguments : ARRAY [STRING]) -- Print arguments passed from the command-line. local i : INTEGER do if arguments = Void then io.put_string ("No arguments received.") io.put_new_line else from i := arguments.lower until i > arguments.upper loop io.put_string ("argument ") io.put_integer (i) io.put_string (": " + arguments.item (i)) io.put_new_line i := i + 1 end end end -- make end -- class ARGUMENTS_WRITER Wird ARGUMENTS_WRITER.make nur als Wurzelprozedur benutzt, so ist der Void-Test für arguments bei vielen Eiffel-Implementationen verzichtbar, weil sie als erstes Argument stets den Kommandonamen übergeben. Da manche Eiffel-Implementationen aber den Kommandonamen nicht übergeben, unterstützt die obige Variante die Portabilität. Allerdings variiert die Anzahl der Argumente je nachdem, ob der Kommandoname mitgezählt wird oder nicht. make hat einen mit local eingeleiteten Vereinbarungsteil, in dem die lokale ganzzahlige Größe i vereinbart ist. Der Anweisungsteil von make enthält im else-Zweig der zweiseitigen Auswahlanweisung (if-then-else-end) eine Schleife: Bedingungsschleife from i := arguments.lower until i > arguments.upper loop io.put_string ("argument ") io.put_integer (i) io.put_string (": " + arguments.item (i)) io.put_new_line i := i + 1 end Eiffel hat als einzige Schleifenart eine kopfgesteuerte Bedingungsschleife mit Initialisierungsteil und Abbruchbedingung der Form Schleifenmuster 27.9.12 from Initialisierungen der Schleifenvariablen until Abbruchbedingung loop Schleifenrumpf end © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 16 2 Einmal quer durch die Sprachen Die obige Schleife dient als Zählschleife; man beachte das explizite Inkrementieren des Zählers i am Rumpfende. Reihung Von der ARRAY-Klasse werden für den minimalen und den maximalen Index und das Element mit dem Index i die Abfragen lower : INTEGER upper : INTEGER item (i : INTEGER) : G benutzt. Für den formalen generischen Parameter G ist aktuell STRING eingesetzt. Eiffel prüft die Gültigkeit von Reihungsindizes optional dynamisch. 2.2.2.2 Konversion make erhält die Kommandoargumente als Zeichenketten, auch wenn es sich um Zahlen handelt. Um Zeichenketten in Zahlen zu konvertieren ist die Zeile io.put_string (": " + arguments.item (i)) durch Mehrseitige Auswahlanweisung io.put_string (": ") if arguments.item (i).is_integer then io.put_string ("integer: "); io.put_integer (arguments.item (i).to_integer) elseif arguments.item (i).is_real then io.put_string ("real: "); io.put_real (arguments.item (i).to_real) else io.put_string ("no integer, no real: " + arguments.item (i)) end zu ersetzen. Die mehrseitige Auswahlanweisung if-then-elseif-else funktioniert genau wie in Component Pascal. Zeichenkette Die Problemlösung ist objektorientiert. Von der STRING-Klasse werden zum Konvertieren die Abfragen is_integer : BOOLEAN is_real : BOOLEAN to_integer : INTEGER to_real : REAL auf die durch arguments.item (i) referenzierten STRING-Objekte angewandt. Seltsamerweise nennt der Eiffel Library Standard Vintage 95 nur die letzten beiden Abfragen, obwohl sie die ersten beiden in Vorbedingungen benötigen.1 Die obige Lösung folgt dem objektorientierten Abfrage-Abfrage-Schema, was bedeutet, sich über den Zustand eines Objekts (unten source) durch eine Abfrage (query) zu informieren und dann, wenn das Objekt in einem zulässigen Zustand ist, weitere Zustandsinformation (unten target) abzufragen: Abfrage-AbfrageSchema -- Get state information from object without precondition: if source.unconditional_query is good then -- Get state information from object with valid precondition: target := source.conditional_query process target else handle bad case end Das Abfrage-Abfrage-Schema lässt sich in jeder imperativen Programmiersprache ohne besondere Sprachkonstrukte anwenden. Sein Vorteil ist Einfachheit, da Miss1 http://www.eiffel-nice.org/standards/nice-2/elks95.pdf (Zugriff 2005-03-21). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2 – 17 erfolge nicht im Nachhinein auftreten können, sondern von vornherein verhindert werden. Das Motto lautet: Lieber vor einem Auftrag fragen, ob dafür alles in Ordnung ist, als sich danach um Misserfolge kümmern müssen. Die Behandlung des unzulässigen Auftrags kann ggf. entfallen. Aufgabe 2.2 Abfrageorientierte Konversion Die Konversion in Eiffel nach dem Abfrage-Abfrage-Schema könnte ineffizient erscheinen, da das Prüfen, ob eine Zeichenkette eine Zahl darstellt, fast denselben Aufwand fordert wie das Konvertieren der Zeichenkette in eine Zahl. Abfrage-AbfrageSchemas in prozeduralem Stil können tatsächlich ineffizient sein. In objektorientiertem Stil formuliert kann der Nachteil verschwinden. Wie lässt sich die abfrageorientierte Konversion effizient implementieren? 2.2.2.3 Entfernte Argumentübernahme Zwar genügt die Argumentübernahme nach Programm 2.20 oft, doch ist sie unhandlich, wenn eine weit von der Wurzelklasse entfernte Klasse auf Kommandozeilen zugreifen soll. Dafür schreibt der Eiffel Library Standard Vintage 95 für alle EiffelImplementationen die Klasse ARGUMENTS mit folgenden Merkmalen vor: command_name : STRING argument_count : INTEGER argument (i : INTEGER) : STRING Der Kommandoname stimmt mit dem 0-ten Argument überein: command_name = argument (0) Problem Bild 2.2 Benutzen und beerben Eine von der Wurzelklasse verschiedene Klasse REMOTE_ARGUMENTS_WRITER soll mit den Merkmalen von ARGUMENTS Kommandoargumente ausgeben. CLIENT_OF_ARGUMENTS use ARGUMENTS inherit DESCENDANT_OF_ARGUMENTS Auf Merkmale einer Klasse lässt sich zugreifen durch Benutzen oder Beerben der Klasse. Welche Art der Strukturierung, die Kunde-Lieferant-Beziehung oder die Vorgänger-Nachfolger-Beziehung, ist hier angemessen? Allgemeiner: In welchen Situationen ist welche Alternative zu wählen? Beerben ist bequemer als Benutzen hinzuschreiben, hier etwa so: Programm 2.21 Eiffel: Argumentübernahme mit Mix-In class REMOTE_ARGUMENTS_WRITER inherit ARGUMENTS feature print_arguments do io.put_string (command_name) end -- print_arguments end -- class REMOTE_ARGUMENTS_WRITER Mix-In-Vererbung 27.9.12 Der inherit-Abschnitt drückt aus, dass REMOTE_ARGUMENTS_WRITER von ARGUMENTS erbt. Das von ARGUMENTS geerbte Merkmal command_name ist in REMOTE_ARGUMENTS_WRITER ohne Qualifizierung mit einem Objektnamen zugreif- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 18 2 Einmal quer durch die Sprachen bar. Aber hier ist Vererbung nicht im Sinne der reinen Lehre eingesetzt (denn ein Argumentenschreiber ist kein Spezialfall von Argumenten), sondern nur als pragmatisches Mix-In, damit REMOTE_ARGUMENTS_WRITER die Merkmale von ARGUMENTS direkt benutzen kann. Folglich ist die Nachfolgerklasse REMOTE_ARGUMENTS_WRITER unnötig eng an die Vorgängerklasse ARGUMENTS gekoppelt, ihre Schnittstelle und ihr Namensraum sind unnötig aufgebläht. Besser ist es, die Leitlinie LS.17 des Dokuments LS Leitlinien zum Softwareentwurf zu beachten: Leitlinie 2.5 Bevorzuge Benutzung vor Mix-Ins Setze Vererbung nur für nicht einschränkende Spezialisierung ein, die die Ist-einRelation und das liskovsche Ersetzungsprinzip erfüllt! Vermeide Mix-Ins; benutze ein Objekt der gewünschten Klasse, anstatt sie zu beerben! Die angemessene Alternative ist hier also Benutzung: Die Kundenklasse REMOTE_ARGUMENTS_WRITER kennt ein Objekt der Lieferantenklasse ARGUMENTS und benutzt dieses Objekt. Dazu ist eine Größe des Typs ARGUMENTS statisch zu vereinbaren und das Objekt, auf das sich die Größe beziehen soll, dynamisch zu erzeugen. Das erfordert etwas mehr Schreibaufwand auch beim Qualifizieren der Zugriffe, den für bessere Softwarequalität – lose Kopplung, saubere Schnittstelle, kleiner Namensraum der Kundenklasse – zu investieren lohnt. Bild 2.3 Kunden-LieferantenKette CLIENT_OF_REMOTE_ARGUMENTS_WRITER REMOTE_ARGUMENTS_WRITER ARGUMENTS CLIENT_OF_REMOTE_ARGUMENTS_WRITER, eine REMOTE_ARGUMENTS_WRITER benutzende Wurzelklasse, komplettiert die Lösung zu einem Beispiel mit drei Klassen und zwei Benutzungsbeziehungen. Programm 2.22 Eiffel: Argumentübernahme irgendwo ohne Mix-In note description: "Output of command arguments" class REMOTE_ARGUMENTS_WRITER feature print_arguments -- Print arguments passed from the command-line. local arguments : ARGUMENTS i : INTEGER do create arguments io.put_string ("command name: " + arguments.command_name) io.put_new_line from i := 0 until i > arguments.argument_count loop io.put_string ("argument " + i.out + ": " + arguments.argument (i)) io.put_new_line i := i + 1 end end -- print_arguments end -- class REMOTE_ARGUMENTS_WRITER In Programm 2.22 hat print_arguments einen lokalen Vereinbarungsteil, in dem durch Größenvereinbarung arguments : ARGUMENTS © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2 – 19 die lokale Größe arguments des Typs ARGUMENTS vereinbart ist. Damit ist die Benutzungsbeziehung zwischen REMOTE_ARGUMENTS_WRITER und ARGUMENTS etabliert. arguments ist eine Referenz, die sich nach ihrer Vereinbarung auf nichts bezieht. Das Objekt, auf das sich arguments beziehen soll, wird durch die Erzeugungsanweisung Objekterzeugung create arguments im Anweisungsteil dynamisch erzeugt. Nachdem sich arguments auf das neue Objekt bezieht, kann es mit dem Punkt dereferenziert und z.B. die Abfrage command_name selektiert werden: Merkmalszugriff arguments.command_name ARGUMENTS ist ein Beispiel für eine Klasse, von der kaum ein Programm mehrere Objekte benötigt. Doch Eiffel unterstützt Singletons und Module nicht durch ein spezielles Sprachkonstrukt. REMOTE_ARGUMENTS_WRITER kommt ohne Erzeugungsprozedur aus, da es nichts zu initialisieren gibt. Benutzung Programm 2.23 Eiffel: Wurzelklasse zur Argumentausgabe Dagegen muss CLIENT_OF_REMOTE_ARGUMENTS_WRITER als Wurzelklasse eine Erzeugungsprozedur besitzen (Programm 2.23). make vereinbart eine lokale Größe writer des Typs REMOTE_ARGUMENTS_WRITER und etabliert so die Benutzungsbeziehung zwischen beiden Klassen. Von Programm 2.22 ist bekannt, dass writer eine Referenz ist, die sich nach ihrer Vereinbarung auf nichts bezieht. Erst nach der Erzeugungsanweisung note description: "Demonstrate usage of class REMOTE_ARGUMENTS_WRITER" class CLIENT_OF_REMOTE_ARGUMENTS_WRITER create make feature make -- Let some object print arguments passed from the command-line. local writer : REMOTE_ARGUMENTS_WRITER do create writer writer.print_arguments end -- make end -- class CLIENT_OF_REMOTE_ARGUMENTS_WRITER Objekterzeugung create writer bezieht sich writer auf das neue dynamisch erzeugte Objekt, sodass es mit dem Punkt dereferenziert und das Kommando print_arguments selektiert werden kann: Merkmalszugriff writer.print_arguments 2.2.3 C++ 2.2.3.1 Argumentübernahme Älter als die obigen Eiffel-Mechanismen ist die Argumentübernahme von der Kommandozeile in C/C++. Programm 2.24 übernimmt beliebig viele Argumente von der Kommandozeile und gibt sie aus. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 20 Programm 2.24 C++: Argumentübernahme Reihung 2 Einmal quer durch die Sprachen #include <iostream> int main (int argc, char * argv[]) { for (int i = 0; i < argc; ++i) { std::cout << "argument " << i << ": " << argv[i] << std::endl; } return 0; } Kommandoargumente werden von der Kommandozeile an main übergeben. Der ganzzahlige Parameter argc von main enthält die Anzahl der Kommandoargumente, die als nullterminierte Zeichenketten in argv[0] bis argv[argc-1] übergeben werden. argc ist erforderlich, weil die Länge von argv nicht dynamisch festzustellen ist. argv ist eine offene Reihung ([]) von Zeigern (*) auf Zeichen (char); pascalisch würde man statt char * argv[] schreiben: argumentVector : ARRAY OF POINTER TO CHAR, und es gälte: LEN (argumentVector) = argumentCount. Die Erbschaft von C, Zeichenketten mit Zeigern auf Zeichen zu bearbeiten, ist fehlerträchtig. Eine weitere Fehlerquelle ist, dass prinzipiell nicht zur Laufzeit prüfbar ist, ob ein Reihungsindex gültig ist. Dies sind zwei der Gründe, weshalb C++ nicht speicherund daher nicht typ- und nicht modulsicher ist. Bei der Argumentübernahme lässt sich die folgende Leitlinie leider nicht anwenden. Leitlinie 2.6 C++: Vermeide unsichere CReihungen Zählschleife Verwende anstelle der unsicheren C-Reihungen und C-Zeichenketten die sicheren Klassen vector und string der Standard Template Library! main gibt die erhaltenen Argumente mit der als Zählschleife benutzten for-Schleife aus: for (int i = 0; i < argc; ++i) { std::cout << "argument " << i << ": " << argv[i] << std::endl; } Die for-Schleife in C++ ist eine kopfgesteuerte Bedingungsschleife mit Vereinbarungsund Initialisierungsteil, Fortsetzungsbedingung und Schleifenrumpfendeausdrucksfolge der Form Schleifenmuster for (initiale Ausdrücke; Fortsetzungsbedingung; Rumpfendeausdrücke) { Schleifenrumpfanfangsanweisungen } Ihre Semantik ist mit einer maschinennahen Sprache der Abstraktionsebene 1.3(2) S. 114 erklärt durch for-Schleife maschinennah Loop: initiale Ausdrücke if not Fortsetzungsbedingung then goto Next Schleifenrumpfanfangsanweisungen Rumpfendeausdrücke goto Loop Next: Diese Art, die Semantik programmiersprachlicher Konstrukte durch Abbilden auf eine einfachere, weniger abstrakte Sprache zu beschreiben, heißt operationale Semantik. Fabel und Fakt Exkurs. Ist die for-Schleife der C*-Sprachen mächtiger als andere Schleifenarten, wie in der Literatur oft behauptet, aber nie bewiesen wird? Die obige operationale Semantik belegt, dass die for-Schleife nicht mehr als eine kopfgesteuerte Bedingungsschleife leistet. Die for-Schleife © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2 – 21 erledigt nichts implizit; alle Aktionen sind vom Programmierer zu spezifizieren, nur ihre textuelle Reihenfolge weicht von ihrer Ausführungsreihenfolge ab. Oben wird durch int i = 0 eine nur in der Schleife gültige Ganzzahlvariable i vereinbart und mit 0 initialisiert. i < argc ist eine Fortsetzungsbedingung, die am Kopf der Schleife ausgewertet wird. Ist der Wert 0, was false entspricht, so wird die Schleife beendet, sonst fortgesetzt. ++ ++i ist ein Ausdruck, der am Ende des Schleifenrumpfs ausgewertet und dessen Wert weggeworfen wird. Sinnvoll ist das nur, wenn der Ausdruck einen Nebeneffekt bewirkt. Hier wird der Wert von i durch den Präinkrementoperator ++ als Nebeneffekt um 1 erhöht, der neue Wert wird als Resultat zurückgegeben und weggeworfen. Ein Test zeigt, dass wie bei Eiffel das 0-te Argument der Kommandoname ist. 2.2.3.2 Konversion Sind die Zeichenketten in Zahlen zu konvertieren, so ist die von C übernommene Standardschnittstellendatei cstdlib.h durch die Vorübersetzerdirektive #include <cstdlib> zu inkludieren und die Zeile std::cout << "argument " << i << ": " << argv[i] << std::endl; durch std::cout << "argument " << i << ": " << argv[i] << ", as int: " << std::atoi(argv[i]) << ", as double: " << std::atof(argv[i]) << std::endl; zu ersetzen. Die Lösung ist funktional, da zum Konvertieren die Funktionen int atoi (const char * string); double atof (const char * string); von cstdlib.h dienen. Es gibt keine Funktion, die prüft, ob eine Zeichenkette eine Ganzoder Gleitpunktzahl enthält. atoi und atof liefern bei Misserfolg 0 bzw. 0.0 zurück. Es ist dann nicht feststellbar, ob 0 eingegeben wurde oder einen Misserfolg anzeigt. Der Entwurf dieser Funktionen aus den 1970er Jahren widerspricht heutigen Softwarequalitätsanforderungen. Die Funktionsnamen aus Unix-Kindheitstagen verstümmeln „argument to integer“ bzw. „argument to float“ auf die damaligen maximal fünf Zeichen (im zweiten Fall obwohl das Ergebnis nicht float, sondern double ist). Eine Größe wie eine Variable, einen Parameter oder ein Funktionsergebnis für mehrere Zwecke zu verwenden, wenn sich die Wertebereiche für die Zwecke überlappen, ist Unfug. Leitlinie 2.7 Ordne jedem Zweck eigene Größen zu Verwende jede Größe für nur einen Zweck! Der Wert jeder Größe muss stets eine eindeutige Bedeutung haben. Dient eine Größe ausnahmsweise zwei Zwecken, so garantiere, dass die Wertebereiche für beide Zwecke disjunkt sind! 2.2.4 Java 2.2.4.1 Argumentübernahme Java erbt und verbessert den C++-Mechanismus der Argumentübernahme, verändert ihn aber insofern, als der Kommandoname nicht als Argument erscheint. Programm 2.25 übernimmt beliebig viele Argumente von der Kommandozeile und gibt sie aus. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 22 Programm 2.25 Java: Argumentübernahme 1 2 Einmal quer durch die Sprachen public class ArgumentsWriter1 { public static void main (String[] args) { for (int i = 0; i < args.length; ++i) { System.out.println("argument " + i + ": " + args[i]); } } } Reihung Wie bei C++ übernimmt main die Argumente von der Aufrufstelle. Der formale Parameter args, der nicht fehlen darf, ist eine offene Reihung von Objekten der Standardbibliotheksklasse String. Unsichere Zeiger entfallen ebenso wie ein Anzahlparameter, da die Länge von args dynamisch abfragbar ist, nämlich durch args.length. Java prüft die Gültigkeit von Reihungsindizes zur Laufzeit. Die for-Schleifen in Java und C# stimmen im Wesentlichen mit der for-Schleife von C++ überein. C#s foreach-Schleife adaptierend führte Java J2SE 5.0 eine Für-alleSchleife (for-each-loop) zum Iterieren durch Behälter ein. In Programm 2.26 iteriert die Für-alle-Schleife, die auch das Schlüsselwort for verwendet, durch die Reihung args, allerdings ohne die Argumente zu zählen. Programm 2.26 Java: Argumentübernahme 2 public class ArgumentsWriter2 { public static void main (String[] args) { for (String s : args) { System.out.println(s); } } } 2.2.4.2 Konversion Sind die Zeichenketten in Zahlen zu konvertieren, so ist die Zeile System.out.println("argument " + i + ": " + args[i]); zu ersetzen durch try { System.out.print("argument " + i + ": " + args[i]); System.out.print(", as float: " + Float.parseFloat(args[i])); System.out.print(", as int: " + Integer.parseInt(args[i])); } catch (Exception e) { // A NumberFormatException is caught. System.out.print(", no float or no int - " + e.toString()); } finally { System.out.println(); } Hüllenklasse Die Lösung kombiniert objektorientiert-modulare und funktionale Elemente mit Ausnahmebehandlung (exception handling). Java nennt die einfachen sprachdefinierten Datentypen primitive Typen. Zu jedem primitiven Typ enthält das Standardbibliothekspaket java.lang eine Hüllenklasse (wrapper class), um Werte primitiver Typen in Objekte packen zu können. Die Hüllenklassen enthalten aber auch viele Elemente, darunter Klassenfunktionen zur Konversion. Von der Hüllenklasse Integer wird public static int parseInt (String s) throws NumberFormatException und von der Hüllenklasse Float wird public static float parseFloat (String s) throws NumberFormatException © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2 – 23 benutzt. Es gibt keine Funktionen zum Prüfen, ob eine Zeichenkette eine Ganz- oder Gleitpunktzahl darstellt. Bei Misserfolg lösen parseInt und parseFloat Zahlenformatausnahmen aus. Für das Verständnis der Ausnahmebehandlung soll hier dies genügen: Wird im try-Block Ausnahmeauslösung try { ... } eine Ausnahme (exception) ausgelöst (geworfen), so verlässt der Ablauf die Auslösestelle direkt, um im catch-Block Ausnahmebehandlung catch (Exception e) { ... } die Ausnahme zu behandeln (aufzufangen). Der optionale finally-Block finally { ... } wird stets ausgeführt: falls keine Ausnahme auftritt nach dem try-Block, sonst nach dem catch-Block (auch wenn eine weitere Ausnahme ausgelöst wird und vor einer return-Anweisung). Danach endet die Ausführung des try-catch-finally-Konstrukts. Die Reihenfolge „erst eine Gleitpunktzahl, dann eine Ganzzahl parsen“ verhindert, dass parseInt eine Ausnahme auslöst, bevor eine Gleitpunktzahl erkannt ist. Leitlinie 2.8 Vermeide leere catchBlöcke Ausnahmebehandlung als Programmierkonzept ermöglicht, angemessen auf im Programmablauf auftretende Fehler zu reagieren. Ausnahmen zeigen Fehler an. Für Ausnahmen eine Behandlung vorzusehen, diese aber leer zu lassen, verschlimmert jeden Fehler und ist daher Unfug. Behandle in jedem catch-Block die möglichen Ausnahmen mit passenden Anweisungen! Das Lösungsschema sei funktionales Schema mit Ausnahme genannt: Funktionales Schema mit Ausnahme /* Try to produce target from any source: */ try { target = service.target(source); // May throw an exception. process target } catch (Exception e) { handle bad case } finally { common action for good and bad case } 2.2.5 C# 2.2.5.1 Argumentübernahme Die erste Lösungsvariante in C#, Programm 2.27, unterscheidet sich kaum von der Java-Variante Programm 2.25. Auch C# lässt den Kommandonamen nicht als Argument erscheinen. Programm 2.27 C#: Argumentübernahme 1 public class ArgumentsWriter1 { public static void Main (string[] args) { for (int i = 0; i < args.Length; ++i) { System.Console.WriteLine("argument {0}: {1}", i, args[i]); } } } Variable Parameteranzahl 27.9.12 WriteLine hat ähnlich wie printf in C eine variable Anzahl von Parametern, deren Werte in Zeichenketten gewandelt in den ersten Parameter eingesetzt werden; in der Ausgabezeile für {0} der Wert von i, für {1} der Wert von args[i]. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 24 2 Einmal quer durch die Sprachen Programm 2.28 C#: Argumentübernahme 2 public class ArgumentsWriter2 { public static void Main (string[] args) { foreach (string s in args) { System.Console.WriteLine(s); } } } Die zweite C#-Variante, Programm 2.28, benutzt die Javas Für-alle-Schleife entsprechende foreach-Schleife, um durch die Reihung args zu iterieren, wie jene ohne die Argumente zu zählen. Syntaktisch unterscheidet sich diese Schleife von jener durch das andere Schlüsselwort und in statt „:“. 2.2.5.2 Konversion Für die Konversion von Zeichenketten in Zahlen bietet C# zwei Varianten, die sich in Details unterscheiden, und seit Version 2.0 eine dritte.1 Die ersten beiden Varianten lösen das Problem möglicher Konversionsmisserfolge wie Java durch Ausnahmebehandlung. Die erste Variante folgt Java auch, indem sie jede Konversionsfunktion der Klasse ihres Zielwerts zuordnet. Bei dieser Variante ist in Programm 2.27 die Zeile System.Console.WriteLine("argument {0}: {1}", i, args[i]); zu ersetzen durch try { System.Console.Write("argument {0}: {1}", i, args[i]); System.Console.Write (", as double: {0}", System.Double.Parse(args[i])); System.Console.Write (", as int: {0}", System.Int32.Parse(args[i])); } catch (System.Exception e) { // An Exception is caught. System.Console.Write(", no double or no int - " + e.Message); } finally { System.Console.WriteLine(); } Double, Int32 und Exception sind Standardbibliotheksklassen im Namensraum System. Die zweite Variante sammelt die Konversionsfunktionen in der Standardbibliotheksklasse System.Convert. Damit ist in Programm 2.27 der Block try { System.Console.Write("argument {0}: {1}", i, args[i]); System.Console.Write (", as double: {0}", System.Convert.ToDouble(args[i])); System.Console.Write (", as int: {0}", System.Convert.ToInt32(args[i])); } catch (System.Exception e) { // An Exception is caught. System.Console.Write(", no double or no int - " + e.Message"); } finally { System.Console.WriteLine(); } einzusetzen. Zum Konvertieren bietet System.Convert die Klassenfunktionen public static int ToInt32 (string value); public static double ToDouble (string value); 1 http://blogs.msdn.com/csharpfaq/archive/2004/05/30/144652.aspx (Zugriff 2007-03-25). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.2 Argumentübernahme von der Kommandozeile 2 – 25 Da System.Convert nur Klassenelemente hat, fungiert es als Modul; die Lösung ist also modular. Bei Misserfolg lösen ToInt32 und ToDouble Zahlenformat- oder Überlaufausnahmen aus, was an ihren Vereinbarungen nicht erkennbar ist. Nachteilig an den beiden ausnahmeorientierten Varianten (wie an der Java-Variante) zur Konversion ist ihre Ineffizienz.1 Deshalb bietet C# 2.0 eine dritte Variante, die an die Component-Pascal-Lösung erinnert, aber statt Prozeduren mit Ein-/AusgabeSchema stilistisch fragwürdige nebeneffektbehaftete Klassenfunktionen der Klassen System.Int32 und System.Double mit einem Ausgabeparameter für die konvertierte Zahl und dem Funktionsergebnis für die Erfolgs-/Misserfolgsanzeige verwendet: public static bool TryParse (string s, out int result); public static bool TryParse (string s, out double result); Auswahlanweisung Nach den möglichen Funktionsergebnissen zu differenzieren erfordert eine Auswahlanweisung. Die ein- und zweiseitige Auswahlanweisung (if, if-else) der C*-Sprachen funktioniert im Wesentlichen wie in Component Pascal. Unterschiede sind, dass in jedem Zweig nur eine Anweisung stehen darf und die Anweisung nicht durch ein Schlüsselwort abgeschlossen ist. Dadurch mögliche Mehrdeutigkeiten werden andernorts diskutiert. Vermeiden kann man sie, indem man stets mit { } geklammerte Blockanweisungen einsetzt; innerhalb der Klammern stehen Anweisungsfolgen. Damit lautet der Block, der in Programm 2.27 einzusetzen ist: System.Console.Write("argument {0}: {1}", i, args[i]); int intArg; double doubleArg; if (System.Int32.TryParse(args[i], out intArg)) { System.Console.WriteLine(", as int: {0}", intArg); } else if (System.Double.TryParse(args[i], out doubleArg)) { System.Console.WriteLine(", as double: {0}", doubleArg); } else { System.Console.WriteLine(", no int, no double"); } C#s Lösung folgt also dem Ein-/Ausgabe-Schema mit Ergebnisrückgabe: Ein-/Ausgabe-Schema mit Ergebnisrückgabe // Try to produce target from any source and // check if target is usable: if (service.compute(source, out target)) { process target } else { handle bad case } Während Hellenen diese Lösung wegen des Nebeneffekts verschmähen, begeistern sich Barbaren dafür, weil sie kein Zwischenspeichern des Ergebnisses und daher keine Vereinbarung einer Ergebnisvariablen erfordert. Leitlinie 2.9 Verwende Ausgabeparameter nur bei Prozeduren 2.2.6 Vermeide Ausgabeparameter bei Funktionen! Funktionen mit Ausgabeparametern bewirken Nebeneffekte, die nach Leitlinie 2.1 zu vermeiden sind. Funktionen sollen ein Ergebnis an die Aufrufstelle liefern und sonst nichts. Fazit Programmaufrufe mit Argumentübergabe erledigt der Kommandospracheninterpretierer der Sprachumgebung (Component Pascal/BlackBox), des Betriebssystems (Eiffel, C*). 1 27.9.12 http://www.developerfusion.co.uk/show/4650/ (Zugriff 2007-03-26). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 26 2 Einmal quer durch die Sprachen Die Anzahl der Argumente ist beschränkt (Component Pascal), unbeschränkt (Eiffel, C*). Die Typen der Argumente sind Zeichenketten (Component Pascal, Eiffel, C*), Ganzzahlen (Component Pascal). Die Argumente empfängt die Prozedur, bei der der Programmablauf startet (Component Pascal, Eiffel, C*), eine beliebige Prozedur (Eiffel). Die Konversion von Zeichenketten in Zahlen übernehmen ein Modul zur Zeichenkettenbearbeitung (Component Pascal), die Zeichenkettenklasse (Eiffel), Funktionen (C++), die Hüllenklassen der Zieltypen (Java, C#), eine allgemeine Konversionsklasse (C#). Die bei der Konversion möglichen Misserfolge werden durch Anwenden des Abfrage-Abfrage-Schemas (Eiffel) von vornherein verhindert oder im Nachhinein angezeigt durch einen Ausgabeparameter (Component Pascal) oder Rückgabewert (C#), einen speziellen Funktionsrückgabewert, der auch ein korrektes Konversionsergebnis sein könnte (C++), oder Auslösen von Ausnahmen (Java, C#). Aufgabe 2.3 Kommandoargumente Testen Sie die Argumentübernahme von der Kommandozeile mit den Programmen von 2.2! Ergänzen Sie die Programme um Konversionen von Zeichenketten in Zahlen! Sprachen: Alle. 2.3 Uhrzeit Problem Am Beispiel der Uhrzeit sieht man die Definition einer datenzentrierten Klasse mit einer vertraglich spezifizierten Schnittstelle, Vertrautheit mit der Methode der Spezifikation durch Vertrag vorausgesetzt. Die Klasse erfüllt bekannte Konsistenzbedingungen und bietet Operationen zum Setzen und Weitersetzen einer Uhrzeit. Da die Standardinitialisierung hier konsistente Objekte liefert, entfallen Initialisierungsproblem, Erzeugungsprozeduren und Konstruktoren. Dafür betrachten wir verschiedene Objektsemantiken wie Wertsemantik mit statisch vereinbarten Objekten (Wertobjekten), die im globalen Datenbereich oder im Laufzeitkeller (runtime stack) liegen, Zeiger- und Referenzsemantik mit dynamisch erzeugten Objekten (Zeiger- bzw. Referenzobjekten), die auf der Halde (heap) liegen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit 2.3.1 Konstante Typ Programm 2.29 Component Pascal: Uhrzeit mit EmpfängerReferenzen 2 – 27 Component Pascal Component Pascal schachtelt Klassen in Module, die zu Subsystemen gehören. Die erste Form der Klassendefinition erlaubt sowohl Wert- als auch Zeigerobjekte. Für eine Klasse wird konventionell ein Verbundtyp und ein zugehöriger Zeigertyp definiert. Konstanten werden nicht in, sondern neben der Klasse im Modul vereinbart. MODULE I3Clocks1; IMPORT BEC := BasisErrorConstants; CONST hoursPerDay* = 24; minutesPerHour* = 60; TYPE Clock* = POINTER TO ClockDesc; ClockDesc* = EXTENSIBLE RECORD hour-, minute- : INTEGER; END; PROCEDURE IsValidTime* (hour, minute : INTEGER) : BOOLEAN; BEGIN RETURN (0 <= hour) & (hour < hoursPerDay) & (0 <= minute) & (minute < minutesPerHour); END IsValidTime; PROCEDURE (VAR clock : ClockDesc) Set* (hour, minute : INTEGER), NEW, EXTENSIBLE; BEGIN ASSERT (IsValidTime (hour, minute), BEC.precondition); clock.hour := hour; clock.minute := minute; ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant); END Set; PROCEDURE (VAR clock : ClockDesc) Tick*, NEW, EXTENSIBLE; BEGIN ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant); clock.minute := (clock.minute + 1) MOD minutesPerHour; IF clock.minute = 0 THEN clock.hour := (clock.hour + 1) MOD hoursPerDay; END; ASSERT (IsValidTime (clock.hour, clock.minute), BEC.invariant); END Tick; END I3Clocks1. Component Pascal erlaubt vollen Export von Datenelementen (Modulvariablen, Verbundfeldern), sodass Kunden lesend und schreibend darauf zugreifen können. Aber gegen Datenabstraktion und die Leitlinie LS.28 des Dokuments LS Leitlinien zum Softwareentwurf zu verstoßen ist selten sinnvoll; ausgenommen sind z.B. Interaktoren zu Dialogen. Leitlinie 2.10 Component Pascal: Verberge Daten hinter Schnittstellen Explizit schreibgeschützter Export 27.9.12 Vermeide Schreibzugriffe erlaubenden Export von Datenelementen, da dies dem Konzept der Datenabstraktion widerspricht! Exportiere Datenelemente höchstens schreibgeschützt, damit die exportierende Einheit die Gültigkeit ihrer Invarianten garantieren kann! Component Pascal differenziert den Export von Diensten nach Zugriffsarten. Datenelemente lassen sich so exportieren, dass Kunden nur lesend darauf zugreifen können. Schreibgeschützter Export © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 28 2 Einmal quer durch die Sprachen ☺ erlaubt eine bequeme Form der Datenabstraktion ohne lästige Programmierung von Typisierter Empfänger Lesezugriffsfunktionen auf Datenelementen, macht die Implementation von Abfragen als Datenelemente an der Schnittstelle sichtbar. Ist z.B. die Abfrage der Stunde als schreibgeschütztes Feld implementiert, so lautet ein Zugriff clock.hour, ist sie als parameterlose Funktion implementiert, so lautet ein Aufruf clock.Hour (). Die unterschiedliche Syntax und die Konvention der Klein-Großschreibung verletzen das Konzept der Bezugstransparenz, der Unabhängigkeit des Zugriffs von der Implementation. Eine Spezialität von Component Pascal ist der frei wählbare Name für das Empfängerobjekt, der konsequenterweise zu typisieren ist – mit dem Verbundtyp oder dem Zeigertyp der Klasse. Daraus ergeben sich zwei Formen der Klassendefinition. Erweiterbarkeit Die Angabe von EXTENSIBLE beim Verbundtyp und von NEW und EXTENSIBLE bei jeder typgebundenen Prozedur scheint verbos. Die Idee bei Component Pascal ist, dass Programmierer von Komponenten abstrakte Klassen definieren und Anwendungsprogrammierer Erweiterungen dieser Klassen implementieren. Die Sprache ist so entworfen, dass die wenigen Komponentenprogrammierer mehr, die vielen Anwendungsprogrammierer weniger schreiben müssen. Konkret: Bei Basistypen sind die Spezifikatoren ABSTRACT, LIMITED, NEW, EXTENSIBLE anzugeben, bei Erweiterungsklassen meist nichts. Spezifikation Als Spezifikationsmittel bietet Component Pascal nur Zusicherungen in Form der Standardprozeduren ASSERT. Damit lassen sich Verträge eingeschränkt formulieren. Komplexe Invarianten sind in Prozeduren zusammenzufassen, was hier unnötig ist. Da die Prüfprozedur IsValidTime unabhängig von Objektdaten ist, ist sie an das Modul, nicht die Klasse gebunden. Die offensichtlichen Nachbedingungen bei der Set-Operation sind weggelassen, um das Beispiel knapp zu halten. Beispiel für Wertobjekt VAR clock : I3Clocks1.ClockDesc; ... clock.Set (12, 30); clock.Tick; (* RECORD, nicht initialisiert *) Beispiel für Zeigerobjekt VAR clock : I3Clocks1.Clock; ... NEW (clock); clock^.Set (12, 30); clock.Tick; (* POINTER TO RECORD *) (* Objekterzeugung, Standardinitialisierung *) (* Explizite Dereferenzierung *) (* Implizite Dereferenzierung *) Die zweite Form der Klassendefinition erlaubt nur Zeigerobjekte. Programm 2.30 Component Pascal: Uhrzeit mit EmpfängerZeigern MODULE I3Clocks2; (* Stuff not shown same as in Programm 2.29. *) TYPE Clock* = POINTER TO EXTENSIBLE RECORD ... END; PROCEDURE (clock : Clock) Set* (hour, minute : INTEGER), NEW, EXTENSIBLE; ... PROCEDURE (clock : Clock) Tick*, NEW, EXTENSIBLE; ... END I3Clocks2. Beispiel für Zeigerobjekt VAR clock : I3Clocks2.Clock; ... NEW (clock); clock^.Set (12, 30); clock.Tick; (* POINTER TO RECORD *) (* Objekterzeugung, Standardinitialisierung *) (* Explizite Dereferenzierung *) (* Implizite Dereferenzierung *) © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit 2.3.2 2 – 29 Eiffel Die in Eiffel übliche Form der Klassendefinition erlaubt Referenzobjekte. Die Struktur von Component Pascal, das in Programm 2.29 die Klasse Clock in das Modul Clocks im Subsystem I3 schachtelt, lässt sich mit Eiffels Sprachmitteln kaum nachbilden. Alle Merkmale gehören zur Klasse. Die Konstanten Hours_per_day, Minutes_per_hour müssen für alle CLOCK-Objekte nur einmal Speicherplatz belegen. Programm 2.31 Eiffel: Uhrzeit nicht expandiert class CLOCK feature Hours_per_day : INTEGER = 24 Minutes_per_hour : INTEGER = 60 hour, minute : INTEGER is_valid_time (an_hour, a_minute : INTEGER) : BOOLEAN do Result := 0 <= an_hour and an_hour < Hours_per_day and 0 <= a_minute and a_minute < Minutes_per_hour end -- is_valid_time set (new_hour, new_minute : INTEGER) require arguments_valid: is_valid_time (new_hour, new_minute) do hour := new_hour minute := new_minute ensure new_hour_accepted: hour = new_hour new_minute_accepted: minute = new_minute end -- set tick do minute := (minute + 1) \\ Minutes_per_hour if minute = 0 then hour := (hour + 1) \\ Hours_per_day end ensure next_minute_set: minute = (old minute + 1) \\ Minutes_per_hour next_hour_set: minute = 0 implies hour = (old hour + 1) \\ Hours_per_day end -- tick invariant state_valid: is_valid_time (hour, minute) end -- class CLOCK Implizit schreibgeschützter Export Die nicht konstanten Attribute hour und minute sind zwar als Teil eines featureAbschnitts ohne Kundenliste öffentlich, aber Eiffel exportiert Attribute prinzipiell schreibgeschützt. Ein Kunde darf bei einer Größe clock der Klasse CLOCK zwar clock.hour abfragen, kann aber nie Zuweisungen der Form clock.hour := 4711 enthalten. Wie Smalltalk setzt Eiffel das Konzept der Datenabstraktion streng um, damit jede Klasse die Gültigkeit ihrer Invarianten garantieren kann. Lesezugriffsfunktionen auf Attribute sind in Eiffel unnötig. Das Konzept der Bezugstransparenz und die Leitlinie LS.28 in LS Leitlinien zum Softwareentwurf werden eingehalten: Am Aufruf 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 30 2 Einmal quer durch die Sprachen clock.hour ist nicht erkennbar, ob die Abfrage hour als Attribut oder als parameterlose Funktion implementiert ist. Erweiterbarkeit Ohne Angaben bei ihrer Definition ist die Klasse beerbbar, ihre Merkmale sind in Nachfolgerklassen redefinierbar. Der Schreibaufwand fällt beim Redefinieren an. Redefinition ist in Eiffel die einzige Möglichkeit, Namen mehrfach zu vereinbaren. Eiffel kennt weder Überladen noch Überdecken. Deshalb müssen sich Formalparameternamen von Attributnamen unterscheiden. Spezifikation durch Vertrag Eiffel ist nicht nur eine Implementations-, sondern auch eine Spezifikationssprache. Es unterstützt die Methode der Spezifikation durch Vertrag (Design by Contract, DbC) mit Klasseninvarianten, Vor- und Nachbedingungen von Routinen, old-Ausdrücken in Nachbedingungen, sowie einigen Spezialitäten. Jede Vertragsbedingung kann zur Dokumentation eine Marke erhalten. Welche Bedingungen in welcher Klasse zur Laufzeit geprüft werden, wird bei der Kompilation festgelegt. Result Auf invariant folgen Invarianten der Klasse, also Bedingungen an ihre Abfragen, die für jedes ihrer Objekte vor und nach jedem Routinenaufruf gelten müssen (außer vor der Objektinitialisierung). Auf require folgen Vorbedingungen der Routine, also Bedingungen an die Abfragen und Routinenparameter, die vor dem Aufruf der Routine gelten müssen. Auf ensure folgen Nachbedingungen der Routine, also Bedingungen an die Abfragen und Routinenparameter, die nach dem Aufruf der Routine gelten müssen, sofern zuvor die Vorbedingungen erfüllt waren. In Nachbedingungen bezeichnet old Expression den Wert von Expression vor der Ausführung des Routinenaufrufs. Result ist eine in jeder Funktion implizit vereinbarte lokale Größe des Resultattyps für den Rückgabewert. Der Wert, den Result beim Erreichen des statischen Endes der Funktion hat, wird als Funktionsergebnis zurückgegeben. Result wird in Nachbedingungen gebraucht. So spart Eiffel eine spezielle Rückkehranweisung. is_valid_time wird u.a. in Invarianten aufgerufen. Vor und nach jeder Ausführung einer Routine können die Invarianten geprüft werden, also auch bei is_valid_time. Das würde zu endlosen rekursiven Aufrufen von is_valid_time führen, aber Eiffel verhindert das. Bei Aufrufen von Abfragen innerhalb von Invariantenprüfungen werden weitere Invariantenprüfungen unterdrückt. \\ Der Modulooperator \\ passt lexikalisch zum Ganzzahldivisionsoperator //. In Eiffel ist Referenzsemantik üblich. Hier ist clock eine Referenz auf ein CLOCKObjekt, das mit einer Erzeugungsanweisung dynamisch auf der Halde angelegt wird: Beispiel für Referenzobjekt clock : CLOCK ... create clock clock.set (12, 30) clock.tick -- Referenz, die sich auf nichts bezieht -- Explizite Objekterzeugung, Standardinitialisierung -- Implizite Dereferenzierung Wertsemantik ist mit der Angabe von expanded bei der Größenvereinbarung möglich:1 Beispiel für Wertobjekt clock : expanded CLOCK ... clock.set (12, 30) clock.tick -- Objekt, per Standard initialisiert Soll eine Klasse nur Wertobjekte ihres Typs ermöglichen, so kann man die Klasse als expandiert spezifizieren. 1 Dieses Sprachmerkmal ist in [ER06], aber nicht im ECMA-Standard [ECMA367] enthalten (Zugriff 2006-04-07). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit Programm 2.32 Eiffel: Uhrzeit expandiert 2 – 31 expanded class CLOCK_EXPANDED -- Stuff not shown same as in Programm 2.31. end -- class CLOCK_EXPANDED Beispiel für Wertobjekt clock : CLOCK_EXPANDED ... clock.set (12, 30) clock.tick -- Objekt, per Standard initialisiert Die Markierung expanded vererbt sich nicht. Expandierte und nicht expandierte Klassen sind beliebig beerbbar, die Nachfolgerklassen können beliebig expandiert oder nicht expandiert sein. Eiffel behandelt Expansion und Vererbung als orthogonale Konzepte. Möglich sind also folgende Definitionen. Programm 2.33 Eiffel: Uhrzeit als expandierter Nachfolger von Programm 2.31 Beispiel für Wertobjekt Programm 2.34 Eiffel: Uhrzeit als nicht expandierter Nachfolger von Programm 2.32 Beispiel für Referenzobjekt 2.3.3 expanded class CLOCK_EXP inherit CLOCK end -- class CLOCK_EXP clock : CLOCK_EXP ... clock.set (12, 30) clock.tick -- Objekt, per Standard initialisiert class CLOCK_REFERENCED inherit CLOCK_EXPANDED end -- class CLOCK_REFERENCED clock : CLOCK_REFERENCED ... create clock clock.set (12, 30) clock.tick -- Referenz, die sich auf nichts bezieht -- Explizite Objekterzeugung, Standardinitialisierung -- Implizite Dereferenzierung C++ Schon Programm 2.7 zeigt, wie der Quelltext einer C++-Klasse auf eine Schnittstellenund eine Implementationsdatei aufzuteilen ist. C++ kennt nur eine Form der Klassendefinition für Wert-, Referenz- und Zeigerobjekte. Component Pascals SubsystemModul-Klasse-Struktur kann C++ mit den geschachtelten Namensräumen I3 und Clocks nachbilden. Die Klassenkonstanten hours_per_day, minutes_per_hour lassen sich ausnahmsweise – da ganzzahligen Typs – innerhalb der Klassendefinition initialisieren. C++, Java und C# erlauben bei Klassen öffentliche Datenelemente, sodass Kunden lesend und schreibend darauf zugreifen können. Aber Klassen mit öffentlichen Daten können die Gültigkeit ihrer Invarianten nicht garantieren. Die C*-Sprachen unterstützen die Leitlinie LS.28 in LS Leitlinien zum Softwareentwurf nicht gut. Leitlinie 2.11 C*: Verberge Daten hinter Schnittstellen Geschütztes Datenelement 27.9.12 Vermeide öffentliche Datenelemente, da dies dem Konzept der Datenabstraktion widerspricht! Vereinbare Datenelemente stets geschützt! Daher sind Datenelemente am besten so zu vereinbaren, dass nur die vereinbarende Klasse und ihre Unterklassen darauf zugreifen können: protected: int hour; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 32 Programm 2.35 C++: Uhrzeit, Schnittstelle 2 Einmal quer durch die Sprachen // Header file: /Include/I3/Clocks/Clock.hpp: #ifndef I3_CLOCKS_CLOCK_HPP_ #define I3_CLOCKS_CLOCK_HPP_ namespace I3 { namespace Clocks { class Clock { protected: int hour, minute; public: static const int hours_per_day = 24; static const int minutes_per_hour = 60; int get_hour () const {return hour;} int get_minute () const {return minute;} static bool is_valid_time (int, int); // an_hour, a_minute ☺ virtual void set (int new_hour, int new_minute); virtual void tick (); }; // end of Clock } // end of Clocks } // end of I3 #endif // I3_CLOCKS_CLOCK_HPP_ Export: alles oder nichts Öffentliche Lesezugriffsfunktion Damit sind sie aber nicht nur schreib-, sondern auch lesegeschützt vor Kundenzugriffen. Datenabstraktion ist realisiert, aber zu stark! C* erlaubt bei Exporten entweder alle Zugriffsarten oder gar keine. Der Schutz lässt sich in C* nicht wie in Component Pascal nach Zugriffsart differenzieren. Um Datenelemente vor externen Schreibzugriffen zu schützen und externe Lesezugriffe darauf zu ermöglichen, definiert man öffentliche Lesezugriffsfunktionen zu den geschützten Datenelementen: public: int get_hour () const {return hour;} Das const nach der Signatur zeigt an, dass die Funktion den Objektzustand belässt. In C++ sind Namen von Datenelementen nicht überladbar. Daher erhält eine Lesezugriffsfunktion einen vom Namen ihres Datenelements abgeleiteten Namen. Üblich ist das Präfix get_ trotz seines Schönheitsfehlers, denn ein Verb passt nicht zu einem Funktionsnamen, der das Funktionsresultat bezeichnen soll. Wegen dieser Konvention werden Lesezugriffsfunktionen oft als Getter-Funktionen oder kurz Getter bezeichnet. Getter-Funktionen sind ein Idiom – ein einfaches sprachabhängiges Programmierschema – der C*-Sprachen. (Manche Autoren werten es zu einem Entwurfsmuster auf.) In C++ implementiert man Zugriffsfunktionen gern als implizite Inline-Funktionen in der Schnittstellendatei, wenn sie nur eine return-Anweisung enthalten. Ihr Vorteil ist, dass ein Inline-Funktionsaufruf clock.get_hour() nicht mehr als ein direkter Lesezugriff clock.hour auf das Datenelement kostet. Nachteile können beim Ableiten (Vererben) auftreten, weil Inline-Funktionen nicht redefinierbar sind und natürlich stets statisch gebunden werden. Im Vergleich erreicht Eiffel mit schreibgeschütztem Export von Attributen dasselbe auf höherem Abstraktionsniveau mit weniger Schreibaufwand. Bei Vereinbarungen von Funktionen sind ihre Signaturen relevant. In C++ kann man die Namen von Formalparametern zwar weglassen, doch mindert dies die Lesbarkeit der Schnittstelle. Leitlinie 2.12 C++: Lesbar statt kurz Gib bei Funktionsvereinbarungen für Formalparameter verständliche Namen an! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit Hellseherprinzip 2 – 33 Klassen sollen wiederverwendbar sein – durch Benutzen und Beerben. Das Hellseherprinzip (clairvoyance principle) besagt, dass der Entwickler einer Klasse nicht voraussehen kann und daher nicht voraussehen müssen sollte, wie die Klasse später wiederverwendet, zu welchen Klassen sie erweitert werden könnte. Deshalb soll eine Klasse zwar ihre Schnittstelle exakt festlegen, aber auch später hinzukommenden Unterklassen erlauben, die spezifizierte Schnittstelle beliebig zu implementieren und zu erweitern. Insbesondere soll eine Klasse die Redefinierbarkeit ihrer Merkmale nur aus zwingenden Gründen einschränken. In C++ muss eine Elementfunktion in der Basisklasse, in der sie zuerst erscheint, als virtual vereinbart sein, damit sie in abgeleiteten Klassen redefinierbar (überschreibbar, overridable) ist und in polymorphen Situationen dynamisch gebunden wird. Der Normalfall objektorientierten Programmierens fordert in C++ besondere Kennzeichnung! Leitlinie 2.13 C++: Denke an Redefinierbarkeit Historisches Programm 2.36 C++: Uhrzeit, Implementation Spezifiziere Elementfunktionen stets als virtual, um sie redefinierbar zu erhalten! Ausgenommen davon sind Inline-Getter-Funktionen. Exkurs. Die Unterscheidung zwischen einerseits so genannten virtuellen, redefinierbaren und dynamisch gebundenen Prozeduren und andererseits nicht-virtuellen, überdeckbaren und statisch gebundenen Prozeduren stammt aus Simula 67, von dessen Klassenmodell C++ viel übernommen hat. Smalltalk hat redefinierbare Methoden, Polymorphie und dynamisches Binden zur Standardsemantik erklärt. Component Pascal, Eiffel, Java und andere objektorientierte Sprachen sind dieser Richtung gefolgt, C# der anderen. // Implementation file: /Source/I3/Clocks/Clock.cpp: #include <cassert> #include "Clock.hpp" namespace I3 { namespace Clocks { bool Clock::is_valid_time (int an_hour, int a_minute) { return 0 <= an_hour && an_hour < hours_per_day && 0 <= a_minute && a_minute < minutes_per_hour; } void Clock::set (int new_hour, int new_minute) { assert(is_valid_time(new_hour, new_minute)); // precondition hour = new_hour; minute = new_minute; assert(is_valid_time(hour, minute)); // invariant } void Clock::tick () { assert(is_valid_time(hour, minute)); // invariant minute = (minute + 1) % minutes_per_hour; if (minute == 0) { hour = (hour + 1) % hours_per_day; } assert(is_valid_time(hour, minute)); // invariant } } // end of Clocks } // end of I3 Spezifikation mit Zusicherung Eine einfache, wenn auch primitiv funktionierende Möglichkeit für Zusicherungen in C++ ist das von C geerbte assert-Vorübersetzermakro; es ist in cassert.h vereinbart. = und == Leicht Verwechslungen verursacht, dass die C*-Sprachen für die Gleichheit statt des in der Mathematik seit rund 350 Jahren weltweit üblichen Zeichens „=“ das „==“ ver- 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 34 2 Einmal quer durch die Sprachen wenden, weil „=“ als Zuweisungsoperator dient. Der Missbrauch des Gleichheitszeichens als Zuweisungssymbol entstammt dem Fortran der 1950er Jahre; schon Algol 60 startete mit dem Zuweisungssymbol „:=“ eine weniger fehlerträchtige Alternative. „=“ und „==“ zu verwechseln wäre allein nicht schlimm. Hinzu kommt die Ausdrucksorientierung der C*-Sprachen, deretwegen die Zuweisung ein Ausdruck ist. Dadurch sind Vergleiche und Zuweisungen nicht syntaktisch unterschieden und können an denselben Stellen auftreten. Bei C++ kommt die schwache Typisierung hinzu, die nicht zwischen booleschen und arithmetischen Ausdrücken differenziert. Wenigstens diesen Mangel beseitigen Java und C#. % „%“ ist in den C*-Sprachen der Modulooperator. Aufgabe 2.4 C++: Falsch getickt Welcher Fehler ist dem Programmierer bei Clock::tick in der Anweisung if (minute = 0) { hour = (hour + 1) % hours_per_day; } unterlaufen und welche Folgen hat er? Ist die Anweisung übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Wie tickt diese Uhr? C++ kennt nur eine Form der Klassendefinition für alle möglichen Speicher- und Zugriffsarten der Objekte. Der Programmierer muss also die spätere Art der Benutzung der Klasse in Form von Objekten nicht voraussehen. Beispiel für Wertobjekt Clock clock = Clock(); Clock clock; ... clock.set(12, 30); clock.tick; // Standardinitialisierung mit Standardkonstruktor // ebenso in abkürzender Notation Beispiel für Referenzobjekt Clock & clock = Clock(); ... clock.set(12, 30); clock.tick(); // Explizite Erzeugung und Initialisierung Clock * clock = new Clock(); ... (*clock).set(12, 30); clock->tick(); // Explizite Erzeugung und Initialisierung Beispiel für Zeigerobjekt 2.3.4 // Implizite Dereferenzierung // Explizite Dereferenzierung // Explizite Dereferenzierung Java Java kennt bei Klassen nur Referenzsemantik, d.h. es erlaubt nur Referenz-, keine Wertobjekte. Component Pascals Subsystem-Modul-Klasse-Struktur lässt sich in Java mit den geschachtelten Paketen I3 und Clocks nachbilden. Namen Java erlaubt das Überladen von Datenelement- und Funktionsnamen. Deshalb heißen die Lesezugriffsfunktionen wie ihre Datenelemente. Namen von Datenelementen und Formalparametern dürfen wie in Component Pascal und C++ gleich sein. Ein Formalparametername überdeckt einen gleichen Datenelementnamen, was die Verständlichkeit mindern kann. Dennoch wird die Möglichkeit bei Setter-Methoden gern genutzt, wobei die überdeckten Namen der Datenelemente mit this, dem Standardnamen der C*-Sprachen für das aktuelle Objekt, zu qualifizieren sind. Spezifikation mit Zusicherung Seit Release 1.4 bietet auch Java Zusicherungen; sie werden mit dem Schlüsselwort assert eingeleitet, dem booleschen Ausdruck kann eine Zeichenkette folgen. Beim Aufruf der JVM ist anzugeben, ob die Zusicherungen zur Laufzeit geprüft werden.1 1 http://java.sun.com/developer/technicalArticles/JavaLP/assertions/ (Zugriff 2008-11-24), http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html (Zugriff 2008-11-24). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit Programm 2.37 Java: Uhrzeit 2 – 35 package I3.Clocks; public class Clock { public static final int HOURS_PER_DAY = 24; public static final int MINUTES_PER_HOUR = 60; private int hour, minute; public int hour () {return hour;} public int minute () {return minute;} public static boolean isValidTime (int hour, int minute) { return 0 <= hour && hour < HOURS_PER_DAY && 0 <= minute && minute < MINUTES_PER_HOUR; } public void set (int hour, int minute) { assert isValidTime(hour, minute) : "precondition"; this.hour = hour; this.minute = minute; assert isValidTime(this.hour, this.minute) : "invariant"; } public void tick () { assert isValidTime(hour, minute) : "invariant"; minute = (minute + 1) % MINUTES_PER_HOUR; if (minute == 0) { hour = (hour + 1) % HOURS_PER_DAY; } assert isValidTime(hour, minute) : "invariant"; } } // end of Clock Aufgabe 2.5 Java: Falsch getickt Welcher Fehler ist dem Programmierer bei Clock.tick in der Anweisung if (minute = 0) { hour = (hour + 1) % HOURS_PER_DAY; } unterlaufen und welche Folgen hat er? Ist die Anweisung übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Wie tickt diese Uhr? Beispiel für Referenzobjekt 2.3.5 Clock clock = new Clock(); // Explizite Erzeugung und Initialisierung ... clock.set(12, 30); // Implizite Dereferenzierung clock.tick(); C# C#s Typsystem unterscheidet zwischen Wert- und Referenztypen. Klassen gehören zu Referenztypen; sie erlauben daher nur Referenzobjekte. Component Pascals Subsystem-Modul-Klasse-Struktur lässt sich in C# mit den geschachtelten Namensräumen I3 und Clocks nachbilden, wobei sich die Schachtelung durch den Punkt ausdrücken lässt. Property Für Setter- und Getter-Funktionen zu Datenelementen bietet C# eine Properties genannte syntaktische Variante (von der Programm 2.38 nur den get-Teil benötigt). Kunden können Properties wie Datenelemente benutzen: h = clock.Hour; clock.Hour = h; 27.9.12 statt statt h = clock.GetHour(); clock.SetHour(h); © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 36 Programm 2.38 C#: Uhrzeit als Referenztyp 2 Einmal quer durch die Sprachen namespace I3.Clocks { public class Clock { public const int HoursPerDay = 24; public const int MinutesPerHour = 60; protected int hour, minute; public int Hour { get {return hour;} } public int Minute { get {return minute;} } public static bool IsValidTime (int hour, int minute) { return 0 <= hour && hour < HoursPerDay && 0 <= minute && minute < MinutesPerHour; } public virtual void Set (int hour, int minute) { System.Diagnostics.Debug.Assert (isValidTime(hour, minute), "precondition"); this.hour = hour; this.minute = minute; System.Diagnostics.Debug.Assert (isValidTime(this.hour, this.minute), "invariant"); } public virtual void Tick () { System.Diagnostics.Debug.Assert (isValidTime(hour, minute), "invariant"); minute = (minute + 1) % MinutesPerHour; if (minute == 0) { hour = (hour + 1) % HoursPerDay; } System.Diagnostics.Debug.Assert (isValidTime(hour, minute), "invariant"); } } // end of Clock } // end of I3.Clocks Namen Leitlinie 2.14 C#: Denke an Redefinierbarkeit Spezifikation mit Zusicherung Anders als Java erlaubt C# kein Überladen von Datenelement-, Funktions- und Propertynamen. Die Namen eines Datenelements und seiner Property unterscheiden sich gern nur in der Klein-Großschreibung des Anfangsbuchstabens. Namen von Datenelementen und Formalparametern dürfen wie in Java übereinstimmen. Spezifiziere Elementfunktionen stets als virtual, um sie redefinierbar zu erhalten! Auch C# bietet Zusicherungen, aber nicht wie Java als Sprachkonstrukt, sondern als Funktion Assert der Bibliotheksklasse Debug, die mit Visual Basic 5.0 in das .NET Framework eingeführt und im Namensraum System.Diagnostics verborgen wurde. Dass die Zusicherungen zur Laufzeit geprüft werden, ist wie bei Eiffel und Java bei der Kompilation festzulegen. Möge der lange vollqualifizierte Name System.Diagnostics.Debug.Assert keinen Programmierer davor abschrecken, Zusicherungen einzusetzen! Eine mögliche Alternative ist, durch die Direktive using System.Diagnostics; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.3 Uhrzeit 2 – 37 vor der Klassendefinition den Namen auf Debug.Assert zu verkürzen, doch wegen der softwaretechnischen Nachteile der using-Direktive rate ich davon ab. Beispiel für Referenzobjekt Wertsemantik Programm 2.39 C#: Uhrzeit als Werttyp Clock clock = new Clock(); // Explizite Erzeugung und Initialisierung ... clock.Set(12, 30); // Implizite Dereferenzierung clock.Tick(); Die C#-Terminologie kennt zwar keine Wertobjekte, man kann aber mit C#s structKonstrukt Werttypen definieren, deren Variablen Datenwerte speichern (die nicht Objekte heißen). In der Typdefinition von Programm 2.38 ist das Schlüsselwort class durch struct zu ersetzen. namespace I3.Clocks { public struct Clock { // Stuff not shown same as in Programm 2.38 but without ’virtual’. } // end of Clock } // end of I3.Clocks Beispiel für Wertexemplar Clock clock = Clock(); ... clock.Set(12, 30); clock.Tick(); // Explizite Initialisierung struct- und andere Werttypen kennen keine Vererbung, sie können weder Ober- noch Untertypen anderer Typen sein. Der Programmierer muss schon bei der Definition eines Typs entscheiden, ob für seine Variablen stets Wertsemantik ohne Erweiterbarkeit (struct) oder stets Referenzsemantik mit Erweiterbarkeit (class) gelten soll. Dies widerspricht dem Hellseherprinzip. C# behandelt Speichersemantik und Erweiterbarkeit als nicht orthogonale Konzepte. 2.3.6 Fazit Eine Definition einer wiederverwendbaren Klasse ist ganz in einer Datei enthalten (Component Pascal, Eiffel, Java, C#), in eine Schnittstellen- und eine Implementationsdatei aufgeteilt (C++), auf mehrere Dateien verteilbar (C++, C#). Klassendefinitionen stehen nebeneinander (Eiffel), sind in andere Sprachkonstrukte geschachtelt: stets zweistufig in Subsysteme und Module (Component Pascal/BlackBox), optional in beliebig viele Namensräume (C++, C#) oder Pakete (Java). Zu einer Klasse gehörende Konstanten werden neben der Klasse definiert und an das Modul gebunden (Component Pascal), in der Klasse definiert und implementationsabhängig bei der Klasse gespeichert, aber an Objekte gebunden (Eiffel), optional mit static bei der Klasse gespeichert und dann an die Klasse und Objekte gebunden (C++, Java), stets bei der Klasse gespeichert und an die Klasse und Objekte gebunden (C#). Wie können Klassen Datenelemente (Felder, Attribute) exportieren? Nur schreibgeschützt (Eiffel), 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 38 2 Einmal quer durch die Sprachen optional schreibgeschützt (Component Pascal), uneingeschränkt, was mit dem Konzept der Datenabstraktion konfligiert (Component Pascal, C++, Java, C#). Die im letzten Fall für Datenabstraktion erforderlichen Getter-Funktionen lassen sich implementieren als Inline-Funktionen (C++), Properties (C#). Das aktuelle Objekt hat einen programmiererdefinierten Namen (Component Pascal), Standardnamen (Current in Eiffel, this in C++, Java, C#). Spezifikation durch Vertrag wird umfassend durch spezielle Sprachkonstrukte unterstützt (Eiffel), eingeschränkt durch Zusicherungen ermöglicht (Component Pascal, C++, Java, C#). Zusicherungen sind realisiert durch eine spezielle Anweisung (Java), Standardprozeduren (Component Pascal), ein Vorübersetzermakro (C++), eine Bibliotheksmethode (C#). Wie bestimmt die Form einer Klassendefinition die Speicher- und Zugriffsarten ihrer Objekte? Eine Form der Klassendefinition erlaubt alle Objektarten: Wert- und Referenzobjekte im globalen Speicher und im Laufzeitkeller, Zeigerobjekte auf der Halde (C++). Eine Form der Klassendefinition erlaubt Wert- und Zeigerobjekte, die andere nur Zeigerobjekte (Component Pascal). Zwei gegenseitig beerbbare Formen der Klassendefinition erlauben Wert- und Referenzobjekte (Eiffel). Eine Form der Klassendefinition erlaubt nur Referenzobjekte (Java). Zwei Formen der Typdefinition erlauben entweder Referenzobjekte und Vererbung oder Wertvariablen und keine Vererbung (C#). 2.4 Aufgaben Aufgabe 2.6 Code verbessern Nennen Sie von Programm 2.40 verletzte Programmierleitlinien und schreiben Sie die C++-Klasse für Uhrzeit so um, dass sie ihre Invariante garantieren kann und die genannten Leitlinien erfüllt! Programm 2.40 C++: Schwache Schnittstelle und class Time { public: int stnd, minut; Implementation void setStunde (int neue_Hour) {stnd=neue_Hour;} void setze_Min (int am) {minut=am;} void tick () {(minut=++minut%60)?0:(stnd=++stnd%24);} }; Hinweis: Die Aufgabe war in der Prüfung Informatik 3 im SS 2007 14 Punkte wert. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.4 Aufgaben 2 – 39 Zusatzfrage: Welche der genannten Programmierleitlinien lassen sich in den anderen Auswahlsprachen auch verletzen, welche nicht? Aufgabe 2.7 Datum Entwerfen, spezifizieren und implementieren Sie eine Klasse für ein Tagesdatum wie 25.10.2007. Sprachen: Alle. Aufgabe 2.8 Englischkenntnisse nachweisen Übersetzen Sie das folgende Zitat aus [Mey09] p. xxxvi ins Deutsche! „Also contributing to the difficulties of using Java in an introductory course are the liberties that the language takes with object-oriented principles. For example: If x denotes an object and a one of the attributes of the corresponding class, you may by default write the assignment x.a = v to assign a new value to the a field of the object. This violates information hiding and other design principles. To rule it out, you must shadow every attribute with a "getter" function. For the teacher, the choice is between forcing students early on to add such noise to their programs, or let them acquire bad design habits which are then hard to unlearn.“ Hinweis: Die Aufgabe war in der Prüfung Informatik 3 im WS 2009/10 10 Punkte wert. Aufgabe 2.9 Problem schematisch lösen Eine Zeichenkette kann einen arithmetischen Ausdruck enthalten. Ausdrücke lassen sich auswerten. Das Prüfen der Ausdruckssyntax und das Auswerten eines Ausdrucks sind etwa gleich aufwändig (vgl. PÜ1 Projektübungsblatt: Interpretierer = Zerteiler + Besucher). Skizzieren Sie zum Problem „Versuch, zu einer Zeichenkette, die einen Ausdruck enthalten kann, den Wert des Ausdrucks zu erhalten“ Entwürfe der Kunde-Lieferant-Protokolle nach den Programmierschemas (1) (2) (3) (4) prozedurales Ein-/Ausgabeschema mit Ergebnisparameter objektorientiertes Abfrage-Abfrage-Schema funktionales Schema mit Ausnahme Ein-/Ausgabeschema mit Ergebnisrückgabe in dokumentiertem Pseudocode, lieferantenseitig mit Vereinbarungen geeigneter Dienste, kundenseitig mit Anweisungen, die diese Dienste benutzen! Nennen Sie die jeweils benötigten Sprachkonstrukte! Hinweis: Die Ausdruckssyntax könnte durch EBNF-Regeln wie Expression Operator Factor digit = Factor { Operator Factor }. = "+" | "-" | "*" | "/". = digit { digit } | "(" Expression ")". = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9". beschrieben sein. Für die Lösung der Aufgabe ist dies belanglos, da hier eine Schnittstelle zu entwerfen, keine Implementation zu codieren ist. Aufgabe 2.10 Chaostheorie – Feigenbaums Periodenverdopplung Die Chaostheorie befasst sich mit dynamischen Systemen, deren Verhalten empfindlich von Anfangsbedingungen abhängt. Solch ein System erscheint als „unstetig“, „ungeordnet“, „unvorhersagbar“, obwohl es deterministisch abläuft und die Beziehungen seiner Elemente oder Zustände durch einfache Regeln oder Gleichungen beschreibbar sind. Ein feines kleines Beispielsystem des Physikers Mitchell Feigenbaum ist durch zwei reelle Zahlen beschrieben. Es bezeichnet s ∈ (0, 1) p ∈ (0, 4) den Zustand, einen Parameter. Ausgehend von einem Anfangszustand s0 ∈ (0, 1) ergeben sich Folgezustände durch si := p ∗ si−1 * (1 − si−1) 27.9.12 für i = 1, 2, 3,... © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 40 2 Einmal quer durch die Sprachen Entwerfen und implementieren Sie ein Java-Applet, mit dem Sie das Verhalten der Folge (si) abhängig von s0 und p untersuchen und textuell und grafisch darstellen können! Dokumentieren Sie die Ergebnisse Ihrer Untersuchung! Verhalten meint: Konvergiert die Folge, oszilliert sie periodisch zwischen mehreren Werten, oder entwickelt sie sich aperiodisch ohne erkennbare Häufungspunkte? Man kann analytisch bestimmen, für welche p die Folge gegen welchen Grenzwert konvergiert. Was zeigt Ihr Applet? Warum? Erkunden Sie Bedingungen für Konvergenz und Formeln für Grenzwerte und validieren Sie sie simulativ mit Zusicherungen! Interessante Parameterwerte sind p = 3, 3.45, 3.55, 3.57, 3.63, 3.84, 3.8568 usw. Ändern Sie p um sehr kleine Beträge! Das Beispiel veranschaulicht Feigenbaums Kaskade der Periodenverdopplung, die bei manchen physikalischen und biologischen Erscheinungen zu beobachten ist. Es gibt einen Wert für p, bei dem Periodizität in Chaos umschlägt. Doch auch im Chaosbereich tritt Periodizität auf – genau betrachtet kommt sogar jede Periode vor. Sprache: Java. Hinweis: Die Aufgabe stammt aus dem Übungsblatt für Wiederholer von Informatik 1 Praktikum im SS 1998. Die Musterlösung findet sich in BlackBox(Hug) in I1Chaos1. Die folgenden Feigenbaum-Diagramme zeigen die Konvergenz- und Häufungspunkte der Folge (si) für verschiedene Anfangszustände s0 und Parameterwerte p. Die waagrechte x-Achse entspricht einem Teilintervall des Parameterbereichs [0, 4), die senkrechte y-Achse den Häufungspunkten von (si) im Intervall (0, 1). Bild 2.4 Feigenbaum-Diagramme © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 2.5 Lösungen 2 – 41 2.5 Lösungen Lösung zu Aufgabe 2.8 Eine Übersetzung des Zitats aus Bertrand Meyers Programmiergrundlagenbuch lautet: „Zu den Schwierigkeiten, Java in einem Einführungskurs einzusetzen, tragen auch die Freiheiten bei, die sich die Sprache bei objektorientierten Prinzipien herausnimmt. Zum Beispiel: Wenn x ein Objekt bezeichnet und a eines der Attribute der zugehörigen Klasse, dann kann man standardmäßig (by default) die Zuweisung x.a = v schreiben, um dem Feld a des Objekts einen neuen Wert zuzuweisen. Das verletzt das Geheimnisprinzip (information hiding) und andere Entwurfsprinzipien. Um dies auszuschließen, muss man jedes Attribut durch eine Lesezugriffsfunktion ("getter" function) verschleiern. Der Lehrer steht vor der Alternative: Entweder zwingt er seine Studenten schon früh dazu, ihren Programmen solches Rauschen hinzuzufügen, oder er lässt sie sich Entwurfsmarotten angewöhnen, die sie sich nur schwer wieder abgewöhnen.“ Bemerkung: Mit „standardmäßig“ ist der Schutzzustand gemeint, der entsteht, wenn das Attribut ohne Schutzmodifikator (also ohne public, protected, private) vereinbart ist. Das Attribut wird dann an alle Klassen im selben Paket exportiert. Allerdings sind in Java public und protected bei Attributen genau so gefährlich. Meyers Kritik trifft auch auf public-Attribute in C++ und C# zu und auf Variablen in Component Pascal, die mit der Exportmarke * versehen sind. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 – 42 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 2 Einmal quer durch die Sprachen 27.9.12 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Aufgabe 3dieser Bild 3 Wurm 3 (3)an Würmern Leitlinie MathAussagen Programm 3 litt, 3 3 DaßBeispiel Tabelle 3 Die wiederum an Würmern litten – Joachim Ringelnatz (1883 – 1934) Der Bandwurm The extent to which the program correctness can be established is not purely a function of the program’s external specifications and behaviour but depends critically upon its internal structure. Edsger W. Dijkstra (1930 – 2002) Notes on Structured Programming (1972) A major function of the structuring of the program is to keep a correctness proof feasible. David Gries On Structured Programming – A Reply to Smoliar (1974) Kleine Kinder haben nicht nur eine große Fähigkeit zur Abstraktion, sondern geradezu einen Instinkt. Denn genau darum geht es beim Erlernen von Sprachen, etwas, was sämtliche Kleinkinder mit Leichtigkeit bewältigen. Keith Devlin Das Mathe-Gen (2003) Dieses Kapitel variiert Euklids berühmten Algorithmus exemplarisch in verschiedenen Programmierstilen und -sprachen, gibt Umformungsschemas an, um eine Lösungsvariante in eine andere zu überführen, und diskutiert Eigenschaften der Varianten wie Verständlichkeit, Verifizierbarkeit, Effizienz und Abstraktionsgrad. Drei Abschnitte befassen sich mit funktionalen, einfachen objektorientierten, und abstrakten objektorientierten Lösungsvarianten. 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional Ganze Zahlen, Teilbarkeit und größter gemeinsamer Teiler (greatest common divisor) ggT : 2 → lN0 , (p, q) |→ ggT(p, q) = max{r ∈ lN0 | r teilt p ∧ r teilt q} zweier Ganzzahlen behandeln die Lehrmodule Theoretische Grundlagen.1 Bekanntlich gelten für ganze Zahlen p, q ∈ die Rechenregeln (3.1) ggT(p, 0) = |p|, (3.2) ggT(p, q) = ggT(q, p), (3.3) q ≠ 0 ⇒ ggT(p, q) = ggT(p mod q, q). 1 Karlheinz Hug: Theoretische Grundlagen. Skript. Hochschule Reutlingen, Fak. Informatik, mki-B (2004) Abschnitt 5.2.3, S. 5-5ff. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 3 – Seite 1 von 60 3–2 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Daraus erhält man die Rekursionsformel (3.4) ⎧p ggT(p, q) = ⎨ ⎩ggT (q, p mod q) falls q = 0 sonst , aus der man den rekursiven euklidischen Algorithmus zur Berechnung des größten gemeinsamen Teilers zweier ganzer Zahlen entwickelt.1 Dieser Algorithmus ist nicht nur über 2000 Jahre alt, sondern immer noch ein lehrreiches Beispiel und wichtig in heutigen Anwendungen, z.B. bei Verschlüsselungsverfahren, die auf der Zahlentheorie beruhen und den größten gemeinsamen Teiler effizient berechnen müssen. Das ggTProblem lautet also: Problem Gegeben: Zwei ganze Zahlen. Gesucht: Der größte gemeinsame Teiler der beiden Zahlen. Entwurf In Programmiersprachen lässt sich der größte gemeinsame Teiler am besten als Funktion ggT, GCD, Gcd oder gcd realisieren. Wie ist die ggT-Funktion, ein rein funktionales Programmteil, von der Benutzungsoberfläche aus zu benutzen? Der zweite Teil der Aufgabe ist, einen kleinen ggT-Rechner zu bauen: ein Ein-/Ausgabe-Programmteil mit einem Aufruf der ggT-Funktion, das zwei Ganzzahlen einnimmt und ihren größten gemeinsamen Teiler ausgibt. Wir beachten die von Leitlinie LS.14 des Dokuments LS Leitlinien zum Softwareentwurf geforderte Trennung von Funktion und Ein-/Ausgabe, ordnen diesen Aspekten bei der ersten Variante zwei Routinen zu, belassen diese aber der Einfachheit halber in derselben Übersetzungseinheit. 3.1.1 Funktionale Sprachen Bevor wir Lösungen in den Auswahlsprachen studieren, nutzen wir die rekursive ggTFunktion als Beispiel für den in 1.2.2 S. 1-9 beschriebenen funktionalen Programmierstil, der sich so charakterisieren lässt: Funktionales Programmieren Alle Berechnungen beruhen auf Funktionen und Ausdrücken. Es gibt keine Variablen mit Speicherplätzen, keine Zuweisungen und keine Schleifen, aber bedingte Ausdrücke und rekursive Funktionsaufrufe. Dazu betrachten wir einige Funktionsdefinitionen und -anwendungen für den größten gemeinsamen Teiler zweier Ganzzahlen in funktionalen Sprachen. Zunächst transformieren wir die Rekursionsformel (3.4) in eiffelischen funktionalen Pseudocode. Programm 3.1 Funktionaler Pseudocode: Größter gemeinsamer Teiler -- Definition: gcd (p, q : INTEGER) : INTEGER is if q = 0 then abs (p) else gcd (q, p mod q) end -- Application: gcd (12, 34) Das Konstrukt Bedingter Ausdruck if Ausdruck1 then Ausdruck2 else Ausdruck3 end 1 Euklid von Alexandria (um 365 – 300 v.u.Z.), griechischer Mathematiker, Physiker, schuf Grundlegendes zu Geometrie, Zahlentheorie, Aufbau und Beweismethodik der Mathematik. Den nach ihm benannten Algorithmus beschreibt er in Elemente, Buch VII in Satz 1 und 2. Übrigens wird in der Literatur oft schlicht vergessen, die Berechnung des ggT(p, q) für negatives p oder q korrekt anzugeben. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3–3 ist ein bedingter Ausdruck (conditional expression) mit der Semantik: Ist der boolesche Ausdruck1 wahr, so wird Ausdruck2, sonst Ausdruck3 ausgewertet und der erhaltene Wert als Wert des bedingten Ausdrucks eingesetzt. Dazu müssen Ausdruck2 und Ausdruck3 vom selben Typ sein. Programm 3.2 Scheme: Größter gemeinsamer Teiler ; Definition: (define (gcd p q) (if (= q 0) (abs p) (gcd q (remainder p q)) ) ) ; Application: (gcd 12 34) Scheme benutzt konsequent kommalose Listen- und Präfixnotation und verzichtet auf syntaktischen Zucker. Im Unterschied zum dynamisch typisierten Scheme ist ML statisch typisiert mit pascalischer Syntax. Programm 3.3 ML: Größter gemeinsamer Teiler 1 (* Definition: *) fun gcd (p, q) : int = if q = 0 then abs (p) else gcd (q, p mod q); (* Application: *) gcd (12, 34) ML ermöglicht auch Funktionen mit alternativen Mustern zu definieren, wobei der senkrechte Strich „|“ die Muster trennt. Programm 3.4 ML: Größter gemeinsamer Teiler 2 (* Definition: *) fun gcd (p, 0) : int = abs (p) | gcd (p, q) : int = gcd (q, p mod q); Ähnlich ML bietet Miranda eine Definitionsform mit Mustern. Programm 3.5 Miranda: Größter gemeinsamer Teiler 1 || Definition: gcd (p, 0) = abs (p) gcd (p, q) = gcd (q, p mod q) || Application: gcd (12, 34) Eine weitere Definitionsform von Miranda, die mit rechts vom definierenden Ausdruck stehenden Wächtern (guard), d.h. Anwendbarkeitsbedingungen arbeitet, ähnelt noch mehr der Rekursionsformel (3.4). Programm 3.6 Miranda: Größter gemeinsamer Teiler 2 || Definition: gcd (p, q) = abs (p), if q = 0 gcd (p, q) = gcd (q, p mod q), otherwise Was lehren die Programme 3.1 bis 3.6? Erstens, dass man zunächst eine korrekte Lösung eines Problems kennen muss, hier die Rekursionsformel (3.4). Zweitens, dass es nachrangig ist, in welcher Sprache man die Lösung notiert. Die Kenntnis funktionaler Programmiertechniken ist auch für imperatives Programmieren nützlich. In imperativen Sprachen, die ein hinreichend allgemeines Funktionskonzept unterstützen, kann man bestimmte Probleme in funktionalem Programmierstil lösen. Dies trifft z.B. auf die hybriden Sprachen Component Pascal und C++ zu. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3–4 Logisches Programmieren 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Kurz abschweifend werfen wir einen Blick auf eine logikbasierte Problemlösung in Prolog, die statt der Funktion ggT eine passende Aussage fordert. Die Gleichung ggT(p, q) = r erfüllt diesen Zweck; es ist nur ein Prädikat dafür zu definieren. Programm 3.7 Prolog: Größter gemeinsamer Teiler % Rules: euclid (p, 0, p) :- p >= 0 euclid (p, 0, gcd) :- p < 0, gcd = -p euclid (p, q, gcd) :- q \= 0, qn is p mod q, euclid (q, qn, gcd) % Goal: ? :- euclid (12, 34, x) 3.1.2 Component Pascal Vom Ausdruck zur Anweisung Wir formulieren zunächst eine rekursive Variante der ggT-Funktion. Programm 3.8 ist in einer imperativen Sprache geschrieben und daher nicht rein funktional: Component Pascal kennt keine bedingten Ausdrücke, nur bedingte Anweisungen. Statt der alternativen Ausdrücke sind alternative RETURN-Anweisungen zu notieren, bei denen freilich nicht das Schlüsselwort RETURN wesentlich ist, sondern die Ausdrücke, deren Werte zurückgegeben werden. Streicht man die RETURNs, so wandelt sich die bedingte Anweisung zum bedingten Ausdruck. Programm 3.8 Component Pascal: Größter gemeinsamer Teiler, rekursiv PROCEDURE GCD* (p, q : INTEGER) : INTEGER; (*! Greatest Common Divisor of p and q. Implemented by Euclid's algorithm, recursive. !*) BEGIN IF q = 0 THEN RETURN ABS (p); ELSE RETURN GCD (q, p MOD q); END; END GCD; Es geht auch ohne ELSE-Zweig: Programm 3.9 Component Pascal: Größter gemeinsamer Teiler, rekursiv Verständlich kontra kurz und unstrukturiert PROCEDURE GCD* (p, q : INTEGER) : INTEGER; BEGIN IF q = 0 THEN RETURN ABS (p); END; RETURN GCD (q, p MOD q); END GCD; In Programm 3.8 treffen die durch die RETURN-Anweisungen bewirkten dynamischen Enden auf das statische Ende der Funktion. Deshalb mag dieser Algorithmus als strukturiert gelten. In Programm 3.9 bildet die erste RETURN-Anweisung im THEN-Zweig ein dynamisches Ende, das sich vom statischen Ende unterscheidet, da eine weitere Anweisung folgt, die im Fall q # 0 ausgeführt wird. Deshalb ist dieser Algorithmus unstrukturiert (er ist nicht mit einem Struktogramm darstellbar). Setzt man ihn unbedarft in Sprachen mit syntaktischen Schwächen um, können Fehler entstehen. Während Programm 3.8 die Alternativen mit der IF-THEN-ELSE-Struktur visualisiert, verschleiert Programm 3.9 die zweite Alternative nach der einseitigen Auswahl als Sequenz – dass es eine Alternative ist, wird erst beim Lesen des THEN-Zweigs klar. Fazit: Programm © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3–5 3.9 ist schwerer verständlich als Programm 3.8. Deshalb sollte man auf fragwürdige Einsparungen von ELSE-Zweigen auf Kosten guter Struktur verzichten. Rekursiv kontra iterativ Rekursive Problemlösungen sind elegant. Nachteilig an ihnen ist jedoch der erhöhte Laufzeitaufwand für die rekursiven Routinenaufrufe durch die damit notwendige Verwaltung des Laufzeitkellers, manchmal auch der erhöhte Speicheraufwand, der den Laufzeitkeller erschöpfen kann. In der imperativen Programmierung bevorzugt man daher effizientere iterative Lösungen – sofern der Verlust an Eleganz vertretbar ist. Zu jedem rekursiven Algorithmus gibt es einen funktional äquivalenten iterativen Algorithmus und umgekehrt, d.h. Rekursion und Iteration sind gleich problemlösungsmächtig. Jeder rekursive Algorithmus lässt sich durch Simulation des Laufzeitkellers in eine iterative Form bringen. Allerdings verlängern Kellerroutinenaufrufe die Laufzeit; expandierte Aufrufe verkürzen die Laufzeit nur wenig. Daher interessieren hinsichtlich Effizienz iterative Varianten rekursiver Algorithmen, die ohne Keller auskommen. Zu manchen rekursiven Einzellösungen existieren effiziente iterative Einzellösungen. Gibt es darüber hinaus Umformungsschemas, die sich auf Kategorien rekursiver Lösungen anwenden lassen? Ja, unter gewissen Bedingungen lassen sich rekursive Algorithmen schematisch in funktional äquivalente iterative Algorithmen ohne Keller umformen. Endrekursion Eine solche Bedingung trifft in Programm 3.8 (und Programm 3.9) zu: Der rekursive Aufruf von GCD steht in einer dynamisch letzten Anweisung der Funktion als Ausdruck, der als letzter vor dem Rücksprung ausgewertet wird; damit handelt es sich um einen endrekursiven Aufruf (rechtsrekursiv, tail recursive call). Eine Funktion oder allgemein Routine, bei der alle rekursiven Aufrufe endrekursiv sind, heißt endrekursiv. Endrekursion (Rechtsrekursion, tail recursion) lässt sich schematisch durch Iteration ersetzen.1 Für Funktionen mit schreibbaren Eingabeparametern lautet das Umformungsschema für endrekursive Algorithmen in iterative Algorithmen: Regel 3.1 Umformung endrekursiver Algorithmen (1) Vereinbare für jeden Formalparameter fp1,.., fpn der rekursiven Funktion eine lokale Variable lv1,.., lvn. (2) Weise vor jedem rekursiven Aufruf den lokalen Variablen die entsprechenden Aktualparameter zu: lv1 := ap1;..; lvn := apn. (3) Weise nach jedem rekursiven Aufruf den Formalparametern die entsprechenden lokalen Variablen zu: fp1 := lv1;..; fpn := lvn. (4) (5) Umgebe den Anweisungsteil mit einer unbedingten Schleife. Streiche die rekursiven Aufrufe. Beweis. Zu zeigen ist die Korrektheit des Schemas, d.h. seine Eigenschaft, jeden endrekursiven Algorithmus in einen äquivalenten iterativen Algorithmus zu überführen. Dazu genügt zu zeigen, dass jeder der fünf Schritte funktionserhaltend ist. Offenbar sind die Schritte (1) bis (3) funktionserhaltend, denn sie ergänzen den rekursiven Algorithmus um Vereinbarungen und Zuweisungen, die nichts am Ergebnis ändern. Ist das Sprachkonstrukt zum Verlassen der Funktion eine RETURN-Anweisung wie in Component Pascal oder den C*-Sprachen, so ist auch Schritt (4) funktionserhaltend, da RETURN aus der scheinbaren Endlosschleife springt. (Sprachen ohne RETURN-Anweisung wie Eiffel fordern hier etwas mehr Überlegung.) Schritt (5) scheint auf den ersten Blick die Funktion zu ändern, aber beim zweiten Blick sieht man, dass die zusätzlichen lokalen Variablen dieselben Werte erhalten wie die Aktualparameter des folgenden rekursiven Aufrufs und damit die Formalparameter, 1 27.9.12 Siehe [Lou93] S. 364, [Set97] S. 188, [GoZ06] 2. Auflage, S. 68. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3–6 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt die zunächst in der nächsten Schachtel auf dem Laufzeitkeller liegen. Vor dem Streichen des rekursiven Aufrufs sind die folgenden Zuweisungen unerreichbar, nach dem Streichen kopieren sie die Werte der lokalen Variablen in die Formalparameter des aktuellen Aufrufs. Dabei werden die alten Werte der Formalparameter überschrieben, was aber nichts an der Funktion ändert, da diese Werte hinter den endrekursiven Aufrufen nicht mehr benötigt werden. Die Formalparameter erhalten also iterativ dieselben Werte, die die Formalparameter der rekursiven Aufrufe erhalten hätten. Programm 3.8 mit Regel 3.1 transformiert ergibt Programm 3.10. Programm 3.10 Component Pascal: Größter gemeinsamer Teiler, umgeformt PROCEDURE GCD* (p, q : INTEGER) : INTEGER; VAR lp, lq : INTEGER; BEGIN LOOP IF q = 0 THEN RETURN ABS (p); ELSE lp := q; lq := p MOD q; RETURN GCD (q, p MOD q); p := lp; q := lq; END; END; END GCD; Prinzipiell kann ein Kompilierer Endrekursionen nach diesem Schema in Schleifen umformen. Tut er es, kann der Programmierer ohne Effizienzverlust endrekursiv codieren. Beispielsweise verlangt die Scheme-Sprachdefinition von Scheme-Implementationen, dass sie echt endrekursiv sind, d.h. unbeschränkt viele aktive Endaufrufe erlauben (z.B. realisierbar durch Schleifen).1 Hier interessiert uns: Lässt sich das schematisch erhaltene iterative Programm 3.10 vereinfachen? Ja – die unbedingte Schleife ergibt mit der Auswahlanweisung im Schleifenrumpf eine kopfgesteuerte Bedingungsschleife. Eine lokale Variable ist verzichtbar (z.B. lp), die zweite benennen wir um (z.B. lq in r). Programm 3.11 Component Pascal: Größter gemeinsamer Teiler, vereinfacht PROCEDURE GCD* (p, q : INTEGER) : INTEGER; VAR r : INTEGER; BEGIN WHILE q # 0 DO r := p MOD q; p := q; q := r; END; RETURN ABS (p); END GCD; Eine weitere kleine Änderung soll die Lesbarkeit verbessern und Probleme vermeiden, die bei negativen Moduluswerten auftreten können, wenn die Modulooperation in der Sprache nicht mathematisch korrekt definiert oder in der Sprachimplementation nicht korrekt realisiert ist (was auf Component Pascal/BlackBox nicht zutrifft). Die Änderung beruht auf der Rechenregel 1 Siehe „5.11. Proper tail recursion“ in: M. Sperber, R. K. Dybvig, M. Flatt, A. Straaten (Eds): Revised6 Report on the Algorithmic Language Scheme. (26. Sept. 2007), S. 20, http:// www.r6rs.org/final/r6rs.pdf (Zugriff 2011-05-18). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3–7 ggT(p, q) = ggT(−p, q) = ggT(p, −q) = ggT(−p, −q). (3.5) Entfernten die bisherigen ggT-Varianten das Minuszeichen erst am Ergebnis, so entfernt Programm 3.12 die Minuszeichen schon an den Eingangswerten. Programm 3.12 Component Pascal: Größter gemeinsamer Teiler, iterativ Effizient kontra klar PROCEDURE GCD* (p, q : INTEGER) : INTEGER; (*! Greatest Common Divisor of p and q. Implemented by Euclid's algorithm, iterative without a selection statement. !*) VAR r : INTEGER; BEGIN p := ABS (p); q := ABS (q); WHILE q > 0 DO r := p MOD q; p := q; q := r; END; RETURN p; END GCD; Was das iterative Programm 3.12 an Effizienz relativ zum rekursiven Programm 3.8 gewinnt, verliert es an Klarheit durch die zusätzliche lokale Variable und die fünf Zuweisungen. Eine Fallunterscheidung statt des Ringtauschs erlaubt eine noch effizientere, aber auch weniger klare Lösungsvariante.1 Dagegen lässt sich mit einer kollektiven Zuweisung, d.h. einer Zuweisung, die mehreren Variablen simultan Werte zuweist, die lokale Variable einsparen und der iterative Algorithmus verkürzen. Programm 3.13 zeigt das aus Algol 68 bekannte Sprachkonstrukt der kollektiven Zuweisung im Kontext von Python, das allerdings eine dynamisch typisierte Sprache ist, sodass dieser Lösungsvariante statische Typsicherheit fehlt. Zudem ist angenommen, dass Python den Modulooperator mathematisch korrekt definiert. Programm 3.13 Python: Größter gemeinsamer Teiler mit kollektiver Zuweisung def gcd (p, q): """Greatest Common Divisor of p and q. Implemented by Euclid's algorithm, iterative with parallel assignments.""" while q != 0: p, q = q, p % q return abs(p) Die kollektive Zuweisung als Vektorzuweisung interpretiert motiviert dazu, das in Regel 3.1 verbalisierte Umformungsschema alternativ in Regel 3.2 mit vereinfachten, formalisierten, austauschbaren Pseudocodemustern darzustellen. Dabei sind x, r(x) Vektoren, „:=“ die Vektorzuweisung, f(x), b(x), s(x) Funktionen, A(x) eine Anweisungsfolge. Regel 3.2 Pseudocode: Umformung eines endrekursiven Funktionsmusters in iterative Funktionsmuster f(x) is if b(x) then return s(x) else A(x); return f(r(x)) end end f(x) is until b(x) loop A(x); x := r(x) end return s(x) end f(x) is while not b(x) loop A(x); x := r(x) end return s(x) end 1 27.9.12 Siehe Component-Pascal-Modul MathArithmeticsI in BlackBox-Informatik-Hug-Subsystemen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3–8 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Nun zum zweiten Teil der Aufgabe: In BlackBox lässt sich der ggT-Rechner am einfachsten durch ein Kommando mit zwei ganzzahligen Parametern realisieren. Programm 3.14 Component Pascal: Rechner für größten gemeinsamen Teiler MODULE I0GCDCalculator; IMPORT StdLog; PROCEDURE GCD* (p, q : INTEGER) : INTEGER; ... END GCD; PROCEDURE Do* (p, q : INTEGER); BEGIN StdLog.Open; StdLog.String ("GCD ("); StdLog.Int (p); StdLog.String (", "); StdLog.Int (q); StdLog.String (") = "); StdLog.Int (GCD (p, q)); StdLog.Ln; END Do; END I0GCDCalculator. Zu benutzen ist der ggT-Rechner durch einen Kommandoaufruf wie ! "I0GCDCalculator.Do (48, 15)" Fehleingabe mit den gewünschten Zahlen. Der BlackBox-Kommandointerpretierer analysiert den Aufruf und führt ihn aus. Fehleingaben wie falsche Anzahl oder nicht ganzzahlige Werte von Parametern behandelt der Kommandointerpretierer. Das Kommando braucht sich nicht um Fehleingaben zu kümmern. 3.1.3 Eiffel Spezifikation Eiffel unterstützt Spezifikation durch Vertrag. Damit können wir die ggT-Funktion zuerst mit Nachbedingungen spezifizieren, die der Rekursionsformel (3.4) entsprechen. Programm 3.15 Eiffel: Größter gemeinsamer Teiler, spezifiziert abs Programm 3.16 Eiffel: Größter gemeinsamer Teiler, rekursiv gcd (p, q : INTEGER) : INTEGER -- Greatest common divisor of p and q. ensure direct_result: q = 0 implies Result = p.abs indirect_result: q /= 0 implies Result = gcd (q, p \\ q) abs ist eine Abfrage der INTEGER-Klasse für den Absolutbetrag. gcd (p, q : INTEGER) : INTEGER -- Greatest common divisor of p and q. -- Implemented by Euclid's algorithm, recursive. do if q = 0 then Result := p.abs else Result := gcd (q, p \\ q) end ensure direct_result: q = 0 implies Result = p.abs indirect_result: q /= 0 implies Result = gcd (q, p \\ q) end -- gcd Programm 3.16 demonstriert die Verwandtschaft zwischen mathematischer Aussage, vertraglicher Spezifikation und funktionaler Lösung. Übrigens gilt bei manchen EiffelImplementationen: Auch wenn Prüfungen von Zusicherungen (wie hier Nachbedingungen) zur Laufzeit angeschaltet sind, werden rekursive Aufrufe in Nachbedingungen © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3–9 nicht ausgeführt. Das vermeidet zwar wiederholte rekursive Aufrufe mit gleichen Parameterwerten, verhindert aber auch, die Korrektheit der Implementation gegen die Spezifikation dynamisch zu prüfen. Programm 3.17 Eiffel: Größter gemeinsamer Teiler, iterativ Schleifeninvariante und -variante gcd (p, q : INTEGER) : INTEGER -- Greatest common divisor of p and q. -- Implemented by Euclid's algorithm, iterative. local s, t : INTEGER do from Result := p.abs s := q.abs invariant 0 <= Result and Result <= p.abs.max (q.abs) 0 <= s and s <= q.abs gcd (Result, s) = gcd (p, q) until s <= 0 loop t := Result \\ s Result := s s := t variant s end end -- gcd Der iterative euklidische Algorithmus in Programm 3.17 zeigt, wie Eiffel-Konstrukte Schleifeninvarianten und -varianten unterstützen. Diese sind Mittel der axiomatischen Semantik, um die Korrektheit von Schleifen zu beweisen. Die von C. A. R. Hoare begründete axiomatische Semantik beschreibt die Semantik von Anweisungskonstrukten durch Bedingungen, insbesondere Vor- und Nachbedingungen. Eine Schleifeninvariante (loop invariant) ist eine Zusicherung, die vor und nach jeder Ausführung des Schleifenrumpfs gilt. Sie gilt also nach der Schleifeninitialisierung, bleibt bei jeder Iteration erhalten, und gilt nach der Schleifenterminierung. Eine Schleifenvariante (loop variant) ist ein natürlichzahliger Ausdruck, dessen Wert sich bei jeder Iteration vermindert. Da er nicht negativ werden kann, garantiert er, dass die Schleife terminiert. Da Formalparameter schreibgeschützt sind (um vertragliche Spezifikation nicht zu erschweren), sind die lokalen Variablen s und t zu vereinbaren, um sich ändernde Werte aufzunehmen. Die Schleifeninvariante 0 <= Result and Result <= p.abs.max (q.abs) and 0 <= s and s <= q.abs ist offensichtlich, aber schwach. Die interessante Invariante gcd (Result, s) = gcd (p, q) entspricht der Rekursionsformel (3.4). Bei angeschalteter Prüfung der Schleifeninvarianten führt sie freilich zu vielen rekursiven Aufrufen. Als Schleifenvariante eignet sich s, das bei jeder Iteration durch s := Result \\ s kleiner wird. Nun sind die Fragmente zum vollständigen Programm eines Kommandozeilen-ggT-Rechners zusammenzustellen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 10 Programm 3.18 Eiffel: Rechner für größten gemeinsamen Teiler 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt note description: "Command-line calculator for greatest common divisor of two integers" class GCD_CALCULATOR create make feature gcd (p, q : INTEGER) : INTEGER -- Greatest common divisor of p and q. -- Implemented by Euclid's algorithm, iterative. local s, t : INTEGER do from Result := p.abs s := q.abs invariant 0 <= Result and Result <= p.abs.max (q.abs) 0 <= s and s <= q.abs gcd (Result, s) = gcd (p, q) until s <= 0 loop t := Result \\ s Result := s s := t variant s end ensure direct_result: q = 0 implies Result = p.abs indirect_result: q /= 0 implies Result = gcd (q, p \\ q) end -- gcd make (arguments : ARRAY [STRING]) -- Given two integer command arguments, print their greatest common divisor. do if arguments = Void or else arguments.count < 3 or else not arguments.item (1).is_integer or else not arguments.item (2).is_integer then io.put_string ("Please call the gcd calculator with two integer arguments!") else io.put_integer (gcd (arguments.item (1).to_integer, arguments.item (2).to_integer)) end io.put_new_line end -- make end -- class GCD_CALCULATOR Auswertung boolescher Ausdrücke Fehleingabe Eine Neuigkeit von Programm 3.18: Eiffel kennt wie Ada zwei Varianten der booleschen Operatoren: Ausdrücke mit and und or werden lang ausgewertet, Ausdrücke mit and then und or else kurz. Dass hier erst die Anzahl der Argumente zu prüfen ist und nur ggf. anschließend ihr Typ, erfordert kurze Auswertung mit or else. Alternativ könnte eine mehrseitige if-elseif-Auswahlanweisung nach verschiedenen Fehleingaben differenzieren. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3.1.4 3 – 11 C++ Die C++-Varianten der ggT-Funktionen erhält man leicht aus den Component-PascalVarianten durch rein syntaktische Änderungen. Programm 3.19 C++: Größter gemeinsamer Teiler, rekursiv abs Bedingter Ausdruck int gcd (int p, int q) { if (q == 0) { return abs(p); } else { return gcd(q, p % q); } } Für die abs-Funktion ist cstdlib.h zu inkludieren. Ersetzen wir in der rekursiven Variante die bedingte Anweisung durch einen bedingten Ausdruck, der in C* mit dem ternären ?:-Operator die Form Bedingung ? Wahrausdruck : Falschausdruck annimmt, so erhalten wir eine fast rein funktionale Variante, die schön kurz ist; nur das Falltrennzeichen „:“ ist etwas unscheinbar. Programm 3.20 C++: Größter gemeinsamer Teiler, fast funktional Historisches Programm 3.21 C++: Größter gemeinsamer Teiler, iterativ int gcd (int p, int q) { return q == 0 ? abs(p) : gcd(q, p % q); } Exkurs. Bedingte Ausdrücke und Rekursion, Konzepte des funktionalen Programmierparadigmas, wurden mit Algol 60 in die imperative Programmierwelt eingeführt und fanden über Algol 68 den Weg nach C und C*. int gcd (int p, int q) { int r; p = abs(p); q = abs(q); while (q > 0) { r = p % q; p = q; q = r; } return p; } Bedingungsschleife Das iterative Programm 3.21 zeigt die kopfgesteuerte while-Bedingungsschleife der C*-Sprachen, die im Wesentlichen wie in Component Pascal funktioniert. Unterschied ist, dass im Schleifenrumpf nur eine Anweisung stehen darf und die Schleifenanweisung (wie fast alle strukturierten Anweisungen der C*-Sprachen) nicht durch ein Schlüsselwort abgeschlossen ist. Daraus resultierende Probleme (Unstetigkeitsstellen, hängende else-Zweige) diskutieren wir anderswo. Mindern kann man sie, indem man als Rumpf stets eine mit { } geklammerte Blockanweisung einsetzt. Aufgabe 3.1 C++: Größter gemeinsamer Teiler, fehlerhaft Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja, welche? int gcd (int p, int q) { return q = 0 ? abs(p) : gcd(q, p % q); } 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 12 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Der Kommandozeilen-ggT-Rechner sieht in C++ so aus: Programm 3.22 C++: Rechner für größten gemeinsamen Teiler // Implementation file: GcdCalculator.cpp #include <cstdlib> #include <iostream> int gcd (int p, int q) { int r; p = abs(p); q = abs(q); while (q > 0) { r = p % q; p = q; q = r; } return p; } int main (int argc, char * argv[]) { if (argc < 3) { std::cout << "Please call the gcd calculator with two integer arguments!"; } else { std::cout << gcd(std::atoi(argv[1]), std::atoi(argv[2])); } std::cout << std::endl; return 0; } Fehleingabe Im Unterschied zum Eiffel-Programm 3.18 beantwortet das C++-Programm 3.22 nur fehlende Argumente mit dem Benutzungshinweis. Bei zwei Argumenten, die beide keine Ganzzahlen darstellen, ist die Antwort 0; ist eines von zwei Argumenten eine Ganzzahl, so erscheint diese als Antwort. Diese Fälle als Fehleingaben zu diagnostizieren wäre relativ aufwändig, weshalb wir hier darauf verzichten – dessen bewusst, dass die Fälle „fehlendes Argument“ und „Argument falschen Typs“ besser einheitlich zu behandeln wären. 3.1.5 Java Die Varianten der ggT-Funktion – rekursiv mit bedingter Anweisung oder bedingtem Ausdruck, oder iterativ – sehen in Java und C# fast so aus wie in C++. Deshalb beschränken wir uns auf die effizientere iterative Variante. In Java nimmt der Kommandozeilen-ggT-Rechner diese Form an: Programm 3.23 Java: Rechner für größten gemeinsamen Teiler public class GcdCalculator { public static int gcd (int p, int q) { int r; p = Math.abs(p); q = Math.abs(q); while (q > 0) { r = p % q; p = q; q = r; } return p; } protected static final String HINT = "Please call the gcd calculator with two integer arguments! "; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3 – 13 public static void main (String[] args) { if (args.length < 2) { System.out.println("Missing Argument. " + HINT); } else { try { System.out.println (gcd (Integer.parseInt(args[0]), Integer.parseInt(args[1]))); } catch (Exception e) { System.out.println ("Wrong Argument. " + HINT + e.toString()); } } } } abs Die abs-Funktion ist eine Klassenfunktion der Standardbibliotheksklasse Math. Fehleingabe Um die verschiedenen Fehleingaben zu differenzieren, wird das if-else für die Prüfung vor dem gcd-Aufruf, das try-catch für den Fehlschlag beim gcd-Aufruf benötigt. Die hybride abfrage- und ausnahmeorientierte Java-Lösung ist damit komplexer als die rein abfrageorientierte Eiffel-Lösung Programm 3.18. Alternativ kann man, um das if-else zu sparen, aus der Not eine Untugend machen und das Prüfen der Argumentanzahl der obligatorischen dynamischen Indexprüfung überlassen, die ggf. eine Ausnahme des Typs ArrayIndexOutOfBoundsException auslöst: Programm 3.24 Java: Ausnahmen bei Fehleingaben public static void main (String[] args) { try { System.out.println (gcd(Integer.parseInt(args[0]), Integer.parseInt(args[1]))); } catch (ArrayIndexOutOfBoundsException e) { System.out.println ("Missing Argument. " + HINT + e.toString()); } catch (NumberFormatException e) { System.out.println ("Wrong Argument. " + HINT + e.toString()); } } Davon rate ich allerdings ab. Mit Ausnahmen sollte man echte Ausnahmefälle behandeln, nicht zwar unpassende, aber durchaus normale Eingaben des Benutzers. Aufgabe 3.2 Java: Größter gemeinsamer Teiler, fehlerhaft Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja, welche? int gcd (int p, int q) { return q = 0 ? Math.abs(p) : gcd(q, p % q); } 3.1.6 Abs 27.9.12 C# Die C#-Variante des Kommandozeilen-ggT-Rechners offenbart keine neuen Aspekte. Die Abs-Funktion ist eine Klassenfunktion der Standardbibliotheksklasse Math im Namensraum System. Wird die Konversion nach dem funktionalen Schema mit Ausnahme gewählt, so ähnelt die Main-Funktion der der Java-Variante Programm 3.23. Bei Konversion nach dem Ein-/Ausgabe-Schema mit Ergebnisrückgabe (S. 2-25) wie in Programm 3.25 nähert sich die Struktur von Main der Eiffel-Variante Programm 3.18. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 14 Programm 3.25 C#: Rechner für größten gemeinsamen Teiler 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt public class GcdCalculator { public static int Gcd (int p, int q) { int r; p = System.Math.Abs(p); q = System.Math.Abs(q); while (q > 0) { r = p % q; p = q; q = r; } return p; } protected const string Hint = "Please call the gcd calculator with two integer arguments!"; public static void Main (string[] args) { if (args.Length < 2) { System.Console.WriteLine("Missing Argument. " + Hint); } else { int p, q; if (System.Int32.TryParse(args[0], out p) && System.Int32.TryParse(args[1], out q)) { System.Console.WriteLine(Gcd(p, q)); } else { System.Console.WriteLine("Wrong Argument. " + Hint); } } } } Aufgabe 3.3 C#: Größter gemeinsamer Teiler, fehlerhaft Welcher Fehler ist dem Programmierer in der folgenden Funktion unterlaufen und welche Folgen hat er? Ist die Funktion übersetzbar? Falls nein, welche Fehlermeldung liefert der Kompilierer? Falls ja, liefert das Laufzeitsystem eine Fehlermeldung? Falls ja, welche? int Gcd (int p, int q) { if (q == 0); return System.Math.Abs(p); return gcd(q, p % q); } 3.1.7 Fazit Die ggT-Funktion lässt sich als Funktion der Programmiersprache vertraglich spezifizieren (Eiffel), rekursiv und iterativ implementieren (Component Pascal, Eiffel, C*). Fehleingaben beim Aufruf des ggT-Rechners behandelt der Kommandointerpretierer der Sprachumgebung (Component Pascal/BlackBox), die aufgerufene Prozedur (Eiffel, C*). Die Anzahl der Argumente wird geprüft durch eine Abfrage (Eiffel, C*). Dass sich ein Zeichenkettenargument nicht in eine Ganzzahl konvertieren lässt, wird vorher durch eine Abfrage geprüft (Eiffel), durch Rückgabe von 0 als Misserfolgswert (C++), oder eines Ergebniswerts (C#) angezeigt, löst eine Ausnahme aus (Java, C#). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.1 Größter gemeinsamer Teiler zweier Ganzzahlen – funktional 3.1.8 Aufgaben Aufgabe 3.4 ggT- und kgV-Rechner (1) (2) (3) 3 – 15 Testen Sie den Kommandozeilen-ggT-Rechner nach 3.1 mit rekursiven und iterativen ggT-Funktionsvarianten! Wenden Sie das Schema zur Umformung endrekursiver in iterative Algorithmen auf die rekursive ggT-Funktion in Eiffel und C* an! Ergänzen Sie das ggT-Programm um eine Funktion für das kleinste gemeinsame Vielfache (kgV; least common multiple, lcm), die Ausgabe des kgV der eingegebenen Zahlen! Sprachen: Alle. Aufgabe 3.5 ggT-Funktion iterativ variiert Programm 3.26 C++: ggT-Funktion 1 Programm 3.27 C++: ggT-Funktion 2 Programm 3.28 C++: ggT-Funktion 3 Programm 3.29 C++: ggT-Funktion 4 27.9.12 In den C*-Sprachen bietet die for-Schleife viele Möglichkeiten, die Schreibung desselben iterativen euklidischen Algorithmus zu variieren. Vier sind unten mit der whileVariante in C++ formuliert. Überlegen Sie sich Kriterien zur Bewertung der fünf Notationsvarianten, bewerten und ordnen Sie die Varianten neu von 1 (beste) bis 5 (schwächste) und begründen Sie die Ordnung! int gcd1 (int p, int q) { int r; p = abs(p); q = abs(q); while (q > 0) { r = p % q; p = q; q = r; } return p; } int gcd2 (int p, int q) { for (int r, p = abs(p), q = abs(q); q > 0; ) { r = p % q; p = q; q = r; } return p; } int gcd3 (int p, int q) { for (int r, p = abs(p), q = abs(q); q > 0; q = r) { r = p % q; p = q; } return p; } int gcd4 (int p, int q) { for (int r, p = abs(p), q = abs(q); q > 0; p = q, q = r) r = p % q; return p; } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 16 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Programm 3.30 C++: ggT-Funktion 5 int gcd5 (int p, int q) { for (int r, p = abs(p), q = abs(q); q > 0; r = p % q, p = q, q = r); return p; } Hinweis: Die Aufgabe war Teil der Prüfung Informatik 3 im WS 2004/05. Sprachen: C*. Aufgabe 3.6 Stellenzahl (1) Programmieren Sie die Funktion number_of_digits (n : INTEGER) : INTEGER -- Number of digits of the decimal representation of n. (2) die die Anzahl der Dezimalziffern ihres Parameters n liefert (Beispiel: number_of_digits (123) = 3), mit einem (a) rekursiven, (b) iterativen Algorithmus! (Aufgabe (b) wurde auch in Informatik 1 Praktikum gestellt.) Falls die rekursive Variante endrekursiv ist, wenden Sie auf sie das Schema zur Umformung endrekursiver in iterative Algorithmen an! Sonst überlegen Sie sich eine Methode, nach der sich die Funktion in eine endrekursive Funktion umformen lässt, sodass Sie den ersten Fall anwenden können!1 Erweitern Sie die Varianten aus (1) um einen zweiten Parameter base für die Basis der Darstellung von n: number_of_digits (n, base : INTEGER) : INTEGER -- Number of digits of the representation of n with respect to the base base. precondition base >= 2 (3) Beispiel: number_of_digits (8, 2) = 4. Programmieren Sie einen Kommandozeilenrechner, der eine ganze Zahl und eine Basis ≥ 2 einnimmt und die Anzahl der Ziffern der Ganzzahl bzgl. der Basis ausgibt! Sprachen: Alle. Aufgabe 3.7 Sprunganweisungen? Nein danke! Informatik 1 bis 3 handeln viel von strukturiertem Programmieren, wenig von Sprüngen. Trotzdem findet der pfiffige Student bald heraus, dass es in den C*-Sprachen die Sprunganweisungen break, continue, manchmal sogar goto gibt, und er benutzt sie sofort. Doch ist die bloße Existenz von Sprunganweisungen in einer Programmiersprache ein guter Grund, Spaghetticode zu produzieren, statt strukturierte Schleifen systematisch zu konstruieren? Ich erläutere die Semantik von break und continue hier, um von ihrem Gebrauch abzuraten und in Informatik-3-Prüfungen Aufgaben stellen zu können, bei denen unstrukturierte Schleifen in strukturierte umzuformen sind. Semantik von break und continue while (...) { ... if (...) break; ... if (...) continue; ... // continue jumps here } // break jumps here 1 for (...; ...; /* there */ ...) { ... if (...) break; ... if (...) continue; ... // continue jumps here and then to there } // break jumps here Ein Beispiel steht in http://de.wikipedia.org/wiki/Endrekursion (Zugriff 2010-11-25). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert Leitlinie 3.1 C*: Vermeide Sprunganweisungen 3 – 17 Verwende goto und continue nie, break höchstens dann in einer Schleife, wenn sie sonst eine zusätzliche Steuervariable oder Kopien von Anweisungen erfordert! Benutze while und do-while für Bedingungsschleifen, for für Zählschleifen, break nur bei unbedingten Schleifen! Konstruiere Bedingungsschleifen systematisch mit ihrer Nachbedingung! Missbrauche Zählschleifen nicht durch Zuweisungen an Zählvariablen! Formen Sie die folgenden C++-Programme schrittweise äquivalent so um, dass (1) (2) (3) (4) Programm 3.31 C++: Beispiel von einer IBM-Webseite Programm 3.32 C++: Beispiel aus der Prüfung Informatik 3 WS 2006/07 weder Sprunganweisungen noch missbrauchte Zählschleifen vorkommen, nach jeder Bedingungsschleife ihre Nachbedingung als Zusicherung steht, möglichst wenige Variablen möglichst lokal vereinbart sind, die Funktion einen aussagekräftigen Namen hat! for (i = 0; i < 5; i++) { if (string[i] == '\0') break; length++; } void g (string s, string & t) { int i, k; bool b; t = ""; for (i = 0; i < s.size(); i++) { b = false; if (i > 0) { for (k = 0; k < t.size(); k++) { if (s[i] == t[k]) { b = true; k = t.size(); } } } if (b == false) { t += s[i]; } } } Sprachen: C*. 3.2 Größter gemeinsamer Teiler – objektorientiert In 3.1 haben wir das Problem des größten gemeinsamen Teilers zweier Ganzzahlen funktional gelöst und rekursive und iterative Implementationen der ggT-Funktion studiert. Zwischen funktionalem und objektorientiertem Programmieren liegen oft nur ein paar Schritte. Um die Verwandtschaft zwischen den Programmierparadigmen zu erkunden, lösen wir nun das ggT-Problem in einem objektorientierten Programmierstil mit einfachen Abstraktionen, und aus software-didaktischen Gründen trennen wir die Aspekte Funktion und Ein-/ Ausgabe stärker als in 3.1 voneinander. Die Darstellung beschränkt sich auf Eiffel und die C*-Sprachen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 18 3.2.1 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Entwurf Der objektorientierte Ansatz packt beim funktionalen Ansatz auftretende Parameter und Resultate in eine Klasse und erzeugt anstelle von Funktionsaufrufen neue Objekte. Für die Lösung des ggT-Problems definieren wir eine Zahlenpaarklasse PAIR_WITH_GCD mit Abfragen p, q und gcd, das für ggT(p, q) steht, und einer Aktion zum Setzen von p und q. Objekte der Klasse sind so zu benutzen: Erst p und q setzen, dann gcd abfragen. Dem liegt das Aktion-Abfrage-Schema zugrunde; es bedeutet, den Zustand eines Objekts durch eine Aktion ändern und sich danach durch Abfragen informieren: Aktion-AbfrageSchema object.action info := object.query -- Change the state of object, but don’t get information from it. -- Get state information from object, but don’t change its state. Offenbar fordert das Aktion-Abfrage-Schema in Abfragen und Aktionen getrennte Dienste der Klasse. Das Konzept der Trennung von Abfragen und Aktionen hat Bertrand Meyer als Teil der Vertragsmethode entwickelt und in Eiffel realisiert (wo Aktionen Kommandos (command) heißen). Im Dokument LS Leitlinien zum Softwareentwurf erscheint es als Leitlinie LS.31. Das Aktion-Abfrage-Schema ist in jeder objektorientierten Programmiersprache anwendbar. Während gelegentlich nachteilig ist, dass sich manche Anwendungsfälle nur umständlicher als mit nebeneffektbehafteten Funktionen formulieren lassen, überwiegen die Vorteile: ☺ Die Aspekte Zustandsänderung und Zustandsabfrage sind sauber getrennt. ☺ Aktionen und Abfragen sind oft einfach. Im Beispiel ist die Aktion eine schlichte Wann p, q setzen? Wie gcd berechnen? Bild 3.1 Kunde mit Schnittstellenklasse und Implementationsklassen Setter-Prozedur, die Abfragen sind parameterlos. Ein PAIR_WITH_GCD-Objekt soll mehrere ggT-Berechnungen nacheinander durchführen können. Dazu muss es jederzeit erlauben, p und q zu setzen, nicht nur bei der Objektvereinbarung oder -erzeugung. gcd berechnet den Wert ggT(p, q) nach rekursivem oder iterativem Muster. Die rekursive Variante von gcd benutzt nur lokale Zahlenpaarobjekte. Es bietet sich an, nach Leitlinie LS.24 und Leitlinie LS.25 des Dokuments LS Leitlinien zum Softwareentwurf von Implementationsvarianten zu abstrahieren und mit einer abstrakten Klasse zu beginnen, die als Schnittstellenklasse die vollständige Schnittstelle der späteren Implementationsklassen festlegt, sodass das Beispiel auch die Konzepte Polymorphie und dynamisches Binden demonstrieren kann. GCD_CLIENT root_action make, main PAIR_WITH_GCD abstract query action p q gcd abstract set PAIR_WITH_GCD_RR PAIR_WITH_GCD_RI query query gcd -- recursive © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 gcd -- iterative 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert Grundbegriffe der OO 3 – 19 Allgemein ist eine Klasse abstrakt, wenn sie mindestens einen abstrakten Dienst enthält. Ein Dienst ist abstrakt, wenn er nicht implementiert ist. Eine Größe ist polymorph, wenn sie sich dynamisch auf Objekte verschiedener Typen beziehen kann. Wird von einer polymorphen Größe ein Dienst aufgerufen, so wird dynamisch gebunden, wenn die im dynamischen Typ der Größe vorliegende Implementation des Dienstes ausgeführt wird. Den Ein-/Ausgabe-Teil, den ggT-Rechner, trennen wir in eine eigene Übersetzungseinheit GCD_CLIENT, die beide Funktionsvarianten benutzen kann. Ein drittes Kommandoargument nach den Ganzzahlen steuert, welche Variante den ggT-Wert berechnet: Kommandozeile gcd_client 12 34 r gcd_client 12 34 Rotwein -- Berechnung mit rekursiver Variante gcd_client 12 34 i gcd_client 12 34 blah gcd_client 12 34 -- Berechnung mit iterativer Variante 3.2.2 Eiffel 3.2.2.1 Schnittstellenklasse Programm 3.33 Eiffel: ggT, objektorientiert abstrakt note description: "Abstraction for two integers and their greatest common divisor" deferred class PAIR_WITH_GCD feature p, q : INTEGER set (new_p, new_q : INTEGER) do p := new_p q := new_q ensure new_p_accepted: p = new_p new_q_accepted: q = new_q end -- set gcd : INTEGER -- Greatest common divisor of p and q. deferred end -- gcd invariant gcd_nonnegative: gcd >= 0 gcd_bounded_by_p: p /= 0 implies gcd <= p.abs gcd_bounded_by_q: q /= 0 implies gcd <= q.abs end -- class PAIR_WITH_GCD deferred Eiffel nennt abstrakte Klassen und Merkmale aufgeschoben, da sie später – in Nachfolgerklassen – implementiert werden, und markiert beide mit dem Schlüsselwort deferred. Die Klasse PAIR_WITH_GCD ist zwar abstrakt, aber partiell implementiert: p und q sind als Attribute, set als Setter-Prozedur fertig gestellt. Die Definition der Abfrage gcd sieht zwar wie eine Attributdefinition aus, doch lässt ihre Markierung als aufgeschoben offen, ob sie später als Attribut oder als parameterlose Funktion implementiert wird. Die schwache Invariante für gcd zeigt exemplarisch, wie sich abstrakte Merkmale vertraglich spezifizieren lassen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 20 3.2.2.2 Programm 3.34 Eiffel: ggT, ooendrekursiv mit Referenzsemantik 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Implementationsklassen note description: "Two integers and their greatest common divisor, referenced recursive" class PAIR_WITH_GCD_RR inherit PAIR_WITH_GCD create set feature gcd : INTEGER -- Implemented by Euclid's algorithm, tail-recursive. local next : PAIR_WITH_GCD_RR do if q = 0 then Result := p.abs else create next.set (q, p \\ q) Result := next.gcd end end -- gcd end -- class PAIR_WITH_GCD_RR Referenzsemantik Objekterzeugung mit expliziter Initialisierung Objekterzeugung mit Standardinitialisierung Die objektorientiert-rekursive Implementationsvariante Programm 3.34 arbeitet wie in Eiffel üblich mit Referenzobjekten; next ist eine lokale Referenz auf ein PAIR_WITH_GCD_RR-Objekt, das mit der Erzeugungsanweisung create next.set (q, p \\ q) dynamisch angelegt wird. Da das geerbte set als Erzeugungsprozedur deklariert ist, muss es bei der Objekterzeugung aufgerufen werden, um das Objekt garantiert korrekt zu initialisieren. Wäre set keine Erzeugungsprozedur, so wären die zwei Anweisungen create next next.set (q, p \\ q) hinzuschreiben: Das Objekt würde bei der Erzeugung mit Standardwerten (0) initialisiert und anschließend mit den gewünschten Werten versehen. In Programm 3.34 ist set nur Erzeugungsprozedur zwecks kompakterer Objekterzeugung mit -initialisierung. Eine Erzeugungsanweisung wie Erzeugungsanweisung create next bewirkt, dass Erzeugungsprozedur ein Objekt des statischen Typs der Zielgröße next dynamisch erzeugt wird, d.h. Speicherplatz auf der Halde dafür reserviert wird, die Attribute des Objekts mit Standardwerten wie False, 0, '', "", Void initialisiert werden, und die Zielgröße das erzeugte Objekt referenziert. Die Standardinitialisierung würde zwar in Programm 3.34 genügen, aber nicht generell. Erzeugungsprozeduren (creation procedure) dienen dem Zweck, Objekte ihrer Klasse so zu initialisieren, dass ihr Anfangszustand konsistent ist, d.h. die Klasseninvariante erfüllt. Folglich dürfen sie die Gültigkeit der Invariante vor ihrem Aufruf nicht voraussetzen. Eiffel-Klassen können beliebig viele Erzeugungsprozeduren mit frei wählbaren Namen und beliebigen Signaturen haben. Hat eine Eiffel-Klasse Erzeu- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert 3 – 21 gungsprozeduren, so muss bei jeder Objekterzeugung eine davon aufgerufen werden. Erzeugungsprozeduren können nach der Objekterzeugung wie andere Prozeduren aufgerufen werden, z.B. zum Reinitialisieren eines Objekts. Müllabfuhr Endet ein Aufruf der gcd-Funktion, so endet auch die Existenz der lokalen Referenz next, d.h. der von ihr belegte Speicherplatz im Laufzeitkeller wird freigegeben. Damit wird das bisher von ihr referenzierte Objekt auf der Halde unerreichbar, belegt aber weiter Speicherplatz. Wer gibt diesen Speicherplatz wann frei? Das besorgt die automatische Speicherbereinigung (garbage collector), die von Zeit zu Zeit hinter den Kulissen Speicherplatz unerreichbarer Objekte einsammelt und freigibt. Den Programmierer braucht das nicht zu kümmern. Effizienz Freilich ist es noch aufwändiger, jedes auftretende Zahlenpaar in ein neues, dynamisch auf der Halde angelegtes Objekt zu packen, als unverpackte Zahlenpaare im Laufzeitkeller zu stapeln wie beim funktional-rekursiven Ansatz! Aufgabe 3.8 Eiffel: ggT, ooendrekursiv mit Wertsemantik Ändern Sie die ineffiziente Zahlenpaarklasse PAIR_WITH_GCD_RR von Programm 3.34 in objektorientiert-rekursive Varianten mit Wertsemantik, bei denen die Objekte dynamisch im Laufzeitkeller angelegt werden, sodass sich ihr Laufzeitaufwand dem der funktional-rekursiven Variante nähert: (1) PAIR_WITH_GCD_VR arbeitet mit einzelnen Wertobjekten (expanded bei der Vereinbarung der Variablen next), (2) PAIR_WITH_GCD_XR ist eine expandierte Klasse (expanded bei der Definition der Klasse), und passen Sie die Kundenklasse GCD_CLIENT so an, dass sie auch die neuen Klassen benutzt! Beachten Sie: Ist next durch expanded (wo auch immer) eine Wertgröße, so werden – da next lokal in der Funktion gcd vereinbart ist – die zu Aufrufen von gcd gehörenden Objekte dynamisch im Laufzeitkeller angelegt. Solche Objekte initialisiert Eiffel implizit mit Standardwerten oder durch eine parameterlose Erzeugungsprozedur. Eiffel erlaubt dabei wegen der Trennung von Vereinbarungen und Anweisungen in Vereinbarungen keine Aufrufe parametrisierter Erzeugungsprozeduren. Daher muss das parametrisierte set darauf verzichten, eine Erzeugungsprozedur zu sein. Das dynamische Erzeugen des Objekts entfällt, es bleibt nur das Initialisieren. Der funktional-iterative Ansatz wiederverwendet dieselben Variablen für die verschiedenen Zahlenwerte und spart so Funktionsaufrufe und Laufzeitkellerverwaltung. Entsprechend suchen wir eine objektorientiert-iterative Lösung, die man in der Praxis dort, wo Effizienz wichtig ist, vorziehen kann. Da die iterative Variante Programm 3.35 keine weiteren Zahlenpaarobjekte und damit kein set benötigt, verzichten wir darauf, set als Erzeugungsprozedur zu deklarieren. Die Schleife ist Programm 3.18 entnommen, ließe sich aber auch aus Programm 3.34 mit dem Umformungsschema von S. 5 konstruieren. Damit ist der Funktionsteil der Problemlösung in zwei Varianten implementiert. Programm 3.35 Eiffel: ggT, oo-iterativ mit Referenzsemantik note description: "Two integers and their greatest common divisor, referenced iterative" class PAIR_WITH_GCD_RI inherit PAIR_WITH_GCD 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 22 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt feature gcd : INTEGER -- Implemented by Euclid's algorithm, iterative. local s, t : INTEGER do from Result := p.abs s := q.abs until s <= 0 loop t := Result \\ s Result := s s := t end end -- gcd end -- class PAIR_WITH_GCD_RI 3.2.2.3 Kundenklasse Den Ein-/Ausgabe-Teil implementieren wir in Eiffel als Wurzelklasse GCD_CLIENT (Programm 3.36). GCD_CLIENT benötigt von jeder Implementationsklasse des Funktionsteils ein Objekt, wofür wir die Variablen pair_rr : PAIR_WITH_GCD_RR pair_ri : PAIR_WITH_GCD_RI lokal in der Wurzelprozedur make vereinbaren könnten. Wir wählen jedoch eine auf allgemeinere Situationen skalierende Entwurfsalternative. Einmalfunktion Programm 3.36 Eiffel: ggT-Rechner, objektorientiert Erstens sind pair_rr und pair_ri keine lokalen Variablen, sondern als Einmalfunktionen (once function) implementierte Abfragen, erkennbar daran, dass ihr Anweisungsteil mit dem Schlüsselwort once statt dem üblichen do beginnt. Dieser Anweisungsteil wird nur einmal für alle Objekte (!) des Typs, zu dem er gehört, ausgeführt, und zwar beim ersten Aufruf der Funktion. Alle Aufrufe liefern dasselbe Ergebnis. Eiffels Einmalfunktionen bilden ein Pendant zu den statischen Methoden der C*-Sprachen. Man kann mit ihnen u.a. Module und Singletons nachbilden. note description: "Command-line calculator for greatest common divisor of two integers" class GCD_CLIENT create make feature {NONE} pair_rr : PAIR_WITH_GCD once create {PAIR_WITH_GCD_RR} Result.set (0, 0) ensure pair_rr_exists: Result /= Void end pair_ri : PAIR_WITH_GCD once create {PAIR_WITH_GCD_RI} Result ensure pair_ri_exists: Result /= Void end © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert 3 – 23 feature make (arguments : ARRAY [STRING]) -- Given two integer command arguments, print their greatest common divisor. -- If 3rd argument starts with 'r' or 'R', then use recursive, else iterative variant. local pair : PAIR_WITH_GCD do if arguments = Void or else arguments.count < 3 or else not arguments.item (1).is_integer or else not arguments.item (2).is_integer then io.put_string ("Please call the gcd client with two integer arguments!") else if arguments.count > 3 and then arguments.item (3).item (arguments.item (3).index_set.lower).as_lower = 'r' then pair := pair_rr else pair := pair_ri end pair.set (arguments.item (1).to_integer, arguments.item (2).to_integer) io.put_integer (pair.gcd) end io.put_new_line end -- make end -- class GCD_CLIENT In diesem Beispiel leisten die Einmalfunktionen dasselbe wie normale Funktionen, da sie in make höchstens einmal aufgerufen werden. Gäbe es jedoch mehrere Aufrufe, so würde eine normale Funktion bei jedem Aufruf ein neues Objekt erzeugen, während die Einmalfunktion nur ein Objekt erzeugt und stets eine Referenz auf dieses liefert. Polymorphe Größe Zweitens sind pair_rr und pair_ri polymorph: Ihr statischer Typ ist PAIR_WITH_GCD, ihr dynamischer Typ PAIR_WITH_GCD_RR bzw. PAIR_WITH_GCD_RI. Die Abfragen verbergen ihren dynamischen Typ hinter ihrer Signatur. Der Kunde – hier make – braucht nur ihren statischen Typ zu kennen, um sie benutzen zu können. Drittens sehen wir eine Variante der Erzeugungsanweisung, mit der ein Objekt eines explizit angegebenen Nachfolgertyps des Typs des Erzeugungsziels erzeugt wird: Objekterzeugung mit explizitem Typ create {PAIR_WITH_GCD_RR} Result.set (0, 0) Das Erzeugungsziel ist Result, sein statischer Typ ist PAIR_WITH_GCD. Der explizite Erzeugungstyp ist PAIR_WITH_GCD_RR, ein Nachfolgertyp von PAIR_WITH_GCD. set ist hier explizit aufzurufen, weil es Erzeugungsprozedur des Erzeugungstyps ist. Dagegen darf set in create {PAIR_WITH_GCD_RI} Result nicht aufgerufen werden, da es keine Erzeugungsprozedur des Erzeugungstyps PAIR_WITH_GCD_RI ist. Beachten Sie die polymorphen Aufrufe Polymorpher Aufruf pair.set (...) pair.gcd in make! 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 24 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt 3.2.3 C++ 3.2.3.1 Schnittstellenklasse Programm 2.7 und Programm 2.35 haben gezeigt, wie in C++ der Quelltext einer Klasse auf eine Schnittstellen- und eine Implementationsdatei aufzuteilen ist. Die Aufteilung gilt auch für die partiell implementierte Schnittstellenklasse für Zahlenpaare. Abstrakt Virtuell In C++ erkennt man abstrakte Klassen nicht an ihrem Kopf, sondern indem man die Deklarationen der Elementfunktionen durchsucht, aber nicht nach einem Schlüsselwort wie abstract, sondern vorne nach virtual und hinten nach Pur Programm 3.37 C++: ggT, objektorientiert abstrakt, Schnittstelle = 0. // Header file: PairWithGcd.hpp #ifndef PAIR_WITH_GCD_HPP_ #define PAIR_WITH_GCD_HPP_ class PairWithGcd { protected: int p, q; public: int get_p () const {return p;} int get_q () const {return q;} virtual void set (int new_p, int new_q); virtual int gcd () const = 0; }; #endif // PAIR_WITH_GCD_HPP_ virtual ist ein Relikt von Simula 67 und bedeutet, dass die Funktion in Ableitungsklas- sen überschreibbar (overridable, d.h. redefinierbar) ist und bei polymorphen Größen (Zeigern, Referenzen) die zu ihrem dynamischen Typ gehörende Version der Funktion dynamisch gebunden wird. „= 0“ wird pure gesprochen und bedeutet, dass die Funktion abstrakt, also hier noch nicht implementiert ist. Stroustrup hat diese pseudoalgebraische Notation erfunden, um ein Schlüsselwort zu sparen. Die Getter-Funktionen get_p, get_q definieren wir wie üblich als nicht virtuelle, konstante Inline-Funktionen in der Klasse. Setter-Funktionen werden auch oft als nicht virtuell definiert. Aber wer kann hellsehen? Wir beachten die Leitlinie 2.13 S. 2-33. Programm 3.38 C++: ggT, objektorientiert abstrakt, partielle Implementation 3.2.3.2 // Implementation file: PairWithGcd.cpp #include "PairWithGcd.hpp" void PairWithGcd::set (int new_p, int new_q) { p = new_p; q = new_q; } Implementationsklassen Die C++-Syntax für Vererbung – Ableitung (derivation) genannt –, ist ein „:“: class PairWithGcdR : public PairWithGcd Das public vor dem Basisklassennamen bedeutet, dass die geerbten Elemente ihren Exportstatus von der Basisklasse übernehmen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert Programm 3.39 C++: ggT, ooendrekursiv, Schnittstelle 3 – 25 // Header file: PairWithGcdR.hpp #ifndef PAIR_WITH_GCD_R_HPP_ #define PAIR_WITH_GCD_R_HPP_ #include "PairWithGcd.hpp" class PairWithGcdR : public PairWithGcd { public: PairWithGcdR (int new_p = 0, int new_q = 0); int gcd () const; }; #endif // PAIR_WITH_GCD_R_HPP_ Konstruktor Die rekursive Implementationsvariante Programm 3.39 benötigt lokale Objekte, die bei ihrer Vereinbarung oder Erzeugung Werte für p und q erhalten sollen. Die Setter-Funktion set genügt dafür nicht, wir müssen zusätzlich einen Konstruktor definieren. In den C*-Sprachen entsprechen Konstruktoren Eiffels Erzeugungsprozeduren. Konstruktoren (constructor) dienen dem Zweck, Objekte ihrer Klasse so zu initialisieren, dass ihr Anfangszustand konsistent ist, haben keine Namen, heißen aber praktisch stets wie ihre Klasse, treten in einer Klasse beliebig zahlreich mit unterschiedlichen Signaturen auf, haben keinen Ergebnistyp (auch nicht void), müssen und dürfen nur bei statischen Objektvereinbarungen und dynamischen Objekterzeugungen aufgerufen werden (dürfen also danach nicht wie normale Funktionen benutzt werden, auch nicht zum Reinitialisieren von Objekten), dürfen Elementfunktionen aufrufen. Die Konstruktordeklaration Standardargument PairWithGcdR (int new_p = 0, int new_q = 0); versieht die formalen Parameter new_p, new_q durch „= 0“ mit Standardargumenten. Fehlen bei einem Konstruktoraufruf aktuelle Parameter, so werden dafür automatisch die Standardargumente eingesetzt. Damit fungiert dieser Konstruktor auch als parameterloser Standardkonstruktor. Die Funktion gcd wird für ihre Definition im Implementationsteil noch einmal deklariert. Sie bleibt nach der Regel „einmal virtuell, immer virtuell“ auch ohne Markierung virtual virtuell. Programm 3.40 C++: ggT, ooendrekursiv, Implementation mit Wertsemantik // Implementation file: PairWithGcdR.cpp #include "PairWithGcdR.hpp" #include <cstdlib> PairWithGcdR::PairWithGcdR (int new_p, int new_q) { set(new_p, new_q); } int PairWithGcdR::gcd () const { if (q == 0) { return abs(p); } else { PairWithGcdR next = PairWithGcdR(q, p % q); return next.gcd(); } } 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 26 Initialisierung 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Der Konstruktor ruft die schon in der Basisklasse implementierte set-Funktion auf. Dass ein Konstruktor andere Elementfunktionen – auch virtuelle! – aufrufen darf, ist nicht unproblematisch, denn diese könnten voraussetzen, dass das Objekt schon initialisiert und die Invariante erfüllt ist. Da C++ keine Unterstützung bietet, um Invarianten zu formulieren und zu prüfen, muss der aufmerksame Programmierer selbst mögliche Henne-Ei-Probleme beim Initialisieren vermeiden. Im Beispiel tritt kein Problem auf. Eine Alternative zum Setter-Aufruf bietet eine Initialisierungsliste, die vor dem dann leeren Anweisungsblock steht und die Standardinitialisierung ersetzt [C++98] 12.6.2 Initializing bases and members S. 197–200: PairWithGcdR::PairWithGcdR (int new_p, int new_q) : p (new_p), q (new_q) {} Diese syntaktische C++-Spezialität ist effizienter als die Initialisierung im Anweisungsblock, repliziert aber Initialisierungscode im Konstruktor und im Setter. Wertsemantik Programm 3.40 arbeitet mit Wertsemantik; sie ist in C++ einfach zu notieren: Die PairWithGcd-Objekte werden durch die Deklaration mit Initialisierungsteil PairWithGcdR next = PairWithGcdR(q, p % q); statisch lokal vereinbart, dynamisch im Laufzeitkeller angelegt und durch einen Konstruktoraufruf initialisiert. Eine abkürzende Notation dafür ist PairWithGcdR next (q, p % q); Zeigersemantik Für die Variante Programm 3.41 mit Zeigersemantik ist gegenüber Programm 3.40 im Rumpf der gcd-Funktion zu ändern: next durch „*“ als Zeiger vereinbaren, das Objekt durch new dynamisch auf der Halde erzeugen, next mit „->“ explizit dereferenzieren und selektieren. Programm 3.41 C++: ggT, ooendrekursiv, Implementation mit Zeigersemantik // Implementation file: PairWithGcdR.cpp ... #include <cassert> int PairWithGcdR::gcd () const { if (q == 0) { return abs(p); } else { PairWithGcdR * next = new PairWithGcdR(q, p % q); assert(next); // new successful. int result = next->gcd(); delete next; return result; } } ... Da C++ die Halde nicht automatisch bereinigt, ist das Objekt nach dem Benutzen durch delete explizit zu löschen. Da die Löschanweisung vor der return-Anweisung stehen muss, ist der Ergebniswert zwischenzuspeichern. Speicherüberlauf Das assert-Vorübersetzermakro prüft, ob next ungleich null ist, d.h. ob new erfolgreich ein Objekt erzeugt hat. Die Objekterzeugung könnte wegen Speichermangel fehlschlagen, denn aus der manuellen Speicherverwaltung in C++ folgt das Problem des Speicherüberlaufs (memory leak) durch nicht gelöschte, unerreichbare Objekte. In C++ löst ein erfolgloses new nicht etwa eine Ausnahme aus, sondern liefert nur null zurück. Da das iterative Programm 3.42 keine Objekte benötigt, verzichten wir auf einen Konstruktor. Die Schleife in Programm 3.43 ist Programm 3.22 entnommen, ließe sich aber © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert 3 – 27 auch aus Programm 3.41 mit dem Umformungsschema von S. 5 konstruieren. Damit ist der Funktionsteil der Problemlösung in zwei Varianten implementiert. Programm 3.42 C++: ggT, oo-iterativ, Schnittstelle // Header file: PairWithGcdI.hpp #ifndef PAIR_WITH_GCD_I_HPP_ #define PAIR_WITH_GCD_I_HPP_ #include "PairWithGcd.hpp" class PairWithGcdI : public PairWithGcd { public: int gcd () const; }; #endif // PAIR_WITH_GCD_I_HPP_ Programm 3.43 C++: ggT, oo-iterativ, Implementation // Implementation file: PairWithGcdI.cpp #include "PairWithGcdI.hpp" #include <cstdlib> int PairWithGcdI::gcd () const { int r = abs(p), s = abs(q), t; while (s > 0) { t = r % s; r = s; s = t; } return r; } 3.2.3.3 Kundenfunktion Die C++-Variante des ggT-Rechners, im Entwurf als GCD_CLIENT bezeichnet, realisieren wir als eine Implementationsdatei mit einer main-Funktion. Für Objekte der Implementationsvarianten der Zahlenklasse könnten wir lokale Variablen in main vereinbaren, bevorzugen aber eine der Eiffel-Variante entsprechende skalierende Alternative. In Programm 3.44 sind pair_r und pair_i durch Konstanter Zeiger static PairWithGcd * const pair_r = new PairWithGcdR(); static PairWithGcd * const pair_i = new PairWithGcdI(); als konstante Zeigervariablen mit obligatorischen Initialisierungsteilen definiert, deren Sichtbarkeit wegen static auf die Datei beschränkt ist, die die Definition enthält. Die Initialisierungen werden einmal beim Start des Programms vor dem Aufruf von main ausgeführt, dabei werden die Objekte dynamisch erzeugt, auf die die Zeiger dann unveränderlich zeigen. Die Objekte existieren bis zum Ende des Programmablaufs. Polymorpher Zeiger pair_r und pair_i sind polymorph: Ihr statischer Zeigerbasistyp ist PairWithGcd, die dynamischen Objekttypen sind durch die Konstruktoraufrufe festgelegt als PairWithGcdR bzw. PairWithGcdI. Die Zeiger verbergen ihren dynamischen Typ vor main, das nur ihren statischen Typ kennt, um sie benutzen zu können. Bei new PairWithGcdR() wird der explizit programmierte Konstruktor mit Standardargumenten aufgerufen, bei new PairWithGcdI() der implizit definierte Standardkonstruktor. Polymorphe Aufrufe in main sind: 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 28 Polymorpher Aufruf Programm 3.44 C++: ggT-Rechner, objektorientiert 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt pair->set(...) pair->gcd() // Implementation file: GcdClient.cpp #include "PairWithGcdR.hpp" #include "PairWithGcdI.hpp" #include <cstdlib> #include <iostream> static PairWithGcd * const pair_r = new PairWithGcdR(); static PairWithGcd * const pair_i = new PairWithGcdI(); int main (int argc, char * argv[]) { if (argc < 3) { std::cout << "Please call the gcd client with two integer arguments!"; } else { PairWithGcd * pair; if (argc > 3 && tolower(*(argv[3])) == 'r') { pair = pair_r; } else { pair = pair_i; } pair->set(std::atoi(argv[1]), std::atoi(argv[2])); std::cout << pair->gcd(); } std::cout << std::endl; return 0; } Programm 3.44 löscht die Objekte an pair_r und pair_i nach ihrer Benutzung nicht. Diese Ordnungswidrigkeit ist hier tolerierbar, weil der Programmablauf gleich endet und das Betriebssystem den vermüllten Speicher zurück erhält. 3.2.4 Java extends Da Java bei Klassen nur Referenzsemantik kennt, folgen weniger Varianten als bei Eiffel und C++. Zudem sind die meisten verwendeten Sprachelemente bekannt. Vererbung heißt in Java Erweiterung (extension) und wird mit dem Schlüsselwort extends ausgedrückt. Javas Exportpolitik müssen wir jedoch näher betrachten. Java bietet vier Schutzzustände für Felder und Methoden von Klassen: Schutz und Zugriffsrechte Der Modifikator public exportiert das markierte Merkmal an alle Klassen. public bei Datenfeldern widerspricht dem Konzept der Datenabstraktion und ist daher zu vermeiden (s. Leitlinie 2.11 S. 2-31). Der Modifikator protected exportiert das Merkmal an alle Klassen im selben Paket und an alle Erweiterungsklassen auch in anderen Paketen. Damit widerspricht auch protected bei Feldern dem Konzept der Datenabstraktion. Eine Klasse kann ihre Invariante nicht allein garantieren, sondern ein Paket ist für die Korrektheit seiner Klassen verantwortlich. Da sich Pakete nicht abschließen, sondern leicht um weitere Klassen erweitern lassen, ist protected problematisch. Ohne Schutzmodifikator wird das Merkmal an alle Klassen im selben Paket exportiert. Bei Feldern widerspricht auch dies dem Konzept der Datenabstraktion und ist so bedenklich wie public und protected. Der Modifikator private exportiert das Merkmal an keine andere Klasse, nicht einmal an Erweiterungsklassen. Solch strenger Schutz ist selten nützlich. Objektorientierte Entwürfe verlangen oft, Zugriffsrechte auf Merkmale an Erweiterungsklassen, aber nicht an Kundenklassen zu vergeben. Was sagt Java dazu? © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert Leitlinie 3.2 Java: Bändige protected stark 3 – 29 Soll eine Klasse Merkmale nur an ihre Erweiterungen, aber nicht an Kunden exportieren, so definiere ein Paket, das nur diese Klasse enthält, und markiere die Merkmale, die an Erweiterungen zu exportieren sind, mit protected. Dass Java (anders als die anderen vier Auswahlsprachen) nur diese Option bietet, befriedigt kaum, da es aufwändig ist, zur Proliferation kleiner Pakete führt und der Idee des Pakets als Ansammlung von Klassen widerspricht. Deshalb schlage ich als Kompromiss vor: Leitlinie 3.3 Java: Bändige protected Soll eine Klasse Merkmale nur an ihre Erweiterungen, nicht an Kunden exportieren, so definiere ein Paket, das nur diese Klasse und Erweiterungen von ihr enthält, und markiere die Merkmale, die an Erweiterungen zu exportieren sind, mit protected. Eine Erweiterungsklasse kann dann zwar auch in einer Kundenrolle auf ein protected Merkmal zugreifen, doch toleriere ich diese Verletzung der Datenabstraktion gegen weniger Aufwand. Den Entwurf von Bild 3.1 setze ich demnach so um: Die Schnittstellenklasse PairWithGcd liegt mit ihren Implementationsklassen PairWithGcdR und PairWithGcdI im Paket Pairs. Die Kundenklasse GcdClient liegt außerhalb des Pakets Pairs. 3.2.4.1 Programm 3.45 Java: ggT, objektorientiert abstrakt Schnittstellenklasse package Pairs; // Only to be used for extensions of PairWithGcd. public abstract class PairWithGcd { protected int p, q; public int p () {return p;} public int q () {return q;} public void set (int p, int q) { this.p = p; this.q = q; } public abstract int gcd (); } Abstrakt Java markiert abstrakte Klassen und Methoden mit dem Modifikator abstract. Alle Methoden werden dynamisch gebunden und sind in Erweiterungsklassen redefinierbar, sofern sie zugreifbar und nicht als final markiert sind. 3.2.4.2 Implementationsklassen Programm 3.46 Java: ggT, ooendrekursiv package Pairs; // Only to be used for extensions of PairWithGcd. public class PairWithGcdR extends PairWithGcd { public PairWithGcdR (int p, int q) { set(p, q); } public int gcd () { if (q == 0) { return Math.abs(p); } else { PairWithGcdR next = new PairWithGcdR(q, p % q); return next.gcd(); } } } 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 30 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Weder das rekursive Programm 3.46 noch das iterative Programm 3.47 bieten Neues. Zu Konstruktoren in Java sagt der C++-Abschnitt 3.3.8 das hier Wesentliche. Durch die Deklaration mit Initialisierungsteil in Programm 3.46 Referenzsemantik PairWithGcdR next = new PairWithGcdR(q, p % q); werden die PairWithGcdR-Objekte dynamisch auf der Halde angelegt und durch einen Konstruktoraufruf initialisiert. Nachdem sie unerreichbar geworden sind, sorgt eine automatische Speicherbereinigung für die Freigabe ihres Speicherplatzes. Programm 3.47 Java: ggT, oo-iterativ package Pairs; // Only to be used for extensions of PairWithGcd. public class PairWithGcdI extends PairWithGcd { public PairWithGcdI (int p, int q) { set(p, q); } public int gcd () { int r = Math.abs(p), s = Math.abs(q), t; while (s > 0) { t = r % s; r = s; s = t; } return r; } } 3.2.4.3 Programm 3.48 Java: ggT-Rechner, objektorientiert Kundenklasse public class GcdClient { protected static final PairWithGcd pairR = new PairWithGcdR(0, 0); protected static final PairWithGcd pairI = new PairWithGcdI(0, 0); protected static final String HINT = "Please call the gcd client with two integer arguments! "; public static void main (String[] args) { if (args.length < 2) { System.out.println("Missing Argument. " + HINT); } else { PairWithGcd pair; if (args.length > 2 && Character.toLowerCase(args[2].charAt(0)) == 'r') { pair = pairR; } else { pair = pairI; } try { pair.set (Integer.parseInt(args[0]), Integer.parseInt(args[1])); System.out.println(pair.gcd()); } catch (Exception e) { System.out.println("Wrong Argument. " + HINT + e.toString()); } } } } Die Java-Variante des ggT-Rechners realisieren wir wie in Eiffel als eine Klasse GcdClient mit einer main-Funktion. Für Objekte der Implementationsvarianten der Zah- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert 3 – 31 lenklasse könnten wir lokale Variablen in main vereinbaren, bevorzugen aber eine der Eiffel-Variante entsprechende skalierende Alternative. pairR Konstante Referenz und pairI sind durch protected static final PairWithGcd pairR = new PairWithGcdR(0, 0); protected static final PairWithGcd pairI = new PairWithGcdI(0, 0); als konstante Referenzen definiert. Die obligatorischen Initialisierungsteile werden einmal beim Programmstart vor dem Aufruf von main ausgeführt, dabei werden die Objekte dynamisch erzeugt, auf die die Referenzen dann unveränderlich zeigen. Polymorphe Referenz und pairI sind polymorph: Ihr statischer Typ ist PairWithGcd, die dynamischen Typen sind durch die Konstruktoraufrufe festgelegt als PairWithGcdR bzw. PairWithGcdI. Auch die Referenzen verbergen ihre dynamischen Typen vor ihrem Kunden main, das nur ihren statischen Typ kennt, um sie benutzen zu können. Polymorphe Aufrufe in main sind: pairR Polymorpher Aufruf 3.2.5 pair.set(...) pair.gcd() C# Da C# zwar Wert- und Referenzsemantik kennt, aber ohne gemeinsame Abstraktionen, folgen mehr Varianten als bei Java mit mehr Schreibaufwand, die teilweise Ihnen als Übung überlassen sind. Da C# (wie C++, aber anders als Java) mit protected markierte Merkmale nur an Erweiterungsklassen exportiert, lassen sich die PairWithGcdKlassen ohne eigenen Namensraum (das C#-Pendant zum Java-Paket) besser als in Java mit eigenem Paket schützen. 3.2.5.1 Programm 3.49 C#: ggT, objektorientiert abstrakt, Referenztyp Schnittstellenklassen public abstract class PairWithGcdR { protected int p, q; public int P {get {return p;}} public int Q {get {return q;}} public virtual void Set (int p, int q) { this.p = p; this.q = q; } public abstract int Gcd (); } Abstrakt Virtuell C# markiert abstrakte Klassen und Methoden wie Java mit dem Modifikator abstract. Aber nur mit virtual oder abstract markierte Methoden werden dynamisch gebunden und sind in Ableitungsklassen redefinierbar. Abstrakte Klassen sind nur als Basisklassen anderer Klassen mit Referenzsemantik nutzbar. Schnittstelle Für Wertsemantik bietet C# Strukturtypen (struct). Gemeinsame Schnittstellenteile von struct-Typen lassen sich in Interfaces (interface) abstrahieren. Von der abstrakten Klasse Programm 3.49 erhalten wir das Interface-Programm 3.50 so: durch interface ersetzen, alle Implementationsteile entfernen, im Einzelnen alle mit protected oder private markierten Elemente, Datenelemente, Anweisungsteile von Properties und Methoden, alle Modifikatoren public, virtual, abstract entfernen. abstract class 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 32 Programm 3.50 C#: ggT, objektorientiert abstrakt, Werttyp 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt public interface PairWithGcdV { int P {get;} int Q {get;} void Set (int p, int q); int Gcd (); } 3.2.5.2 Programm 3.51 C#: ggT, ooendrekursiv, Referenztyp Implementationsklassen public class PairWithGcdRR : PairWithGcdR { public PairWithGcdRR (int p, int q) { Set(p, q); } public override int Gcd () { if (q == 0) { return System.Math.Abs(p); } else { PairWithGcdR next = new PairWithGcdRR(q, p % q); return next.Gcd(); } } } : override Bei Klassen – den Referenztypen – spricht C# von Vererbung und Ableitung und drückt die Beziehung durch „:“ aus. Das rekursive Programm 3.51 bietet kaum Neues: Redefinitionen von Methoden müssen mit override markiert werden. Sonst gelten die Bemerkungen zur Java-Variante auf S. 29f. Aufgabe 3.9 C#: ggT, oo-iterativ, Referenztyp Ändern Sie die ineffiziente Zahlenpaarklasse PairWithGcdRR von Programm 3.51 in eine objektorientiert-iterative Variante PairWithGcdRI mit Referenzsemantik! Programm 3.52 C#: ggT, oo-iterativ, Werttyp public struct PairWithGcdVI : PairWithGcdV { private int p, q; public int P {get {return p;}} public int Q {get {return q;}} public void Set (int p, int q) { this.p = p; this.q = q; } public int Gcd () { int r = System.Math.Abs(p), s = System.Math.Abs(q), t; while (s > 0) { t = r % s; r = s; s = t; } return r; } } Wertsemantik Die Wertsemantik-Variante Programm 3.52 zeigt zur Abwechslung die iterative Implementation. Strukturtypen können zwar nicht erben, aber wenigstens Interfaces implementieren. Auch diese Beziehung drückt C# durch „:“ aus. Die aus dem Interface-Programm 3.50 entfernten Implementationsteile erscheinen wieder im implementierenden © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.2 Größter gemeinsamer Teiler – objektorientiert 3 – 33 Strukturtyp. Allerdings darf ein struct-Konstruktor keine Methode aufrufen. Deshalb verzichten wir auf einen programmiererdefinierten Konstruktor und begnügen uns mit dem Standardkonstruktor. Zu erwähnen bleibt, dass Datenelemente nicht protected sein dürfen (da Stukturtypen erbenlos sind), folglich als private zu markieren sind. Aufgabe 3.10 C#: ggT, oo-rr, Werttyp Ändern Sie den Zahlenpaartyp PairWithGcdVI von Programm 3.52 in eine objektorientiert-endrekursive Variante PairWithGcdVR mit Wertsemantik! 3.2.5.3 Kundenklasse Die C#-Variante des ggT-Rechners realisieren wir wie in Eiffel und Java als eine Klasse GcdClient mit einer Main-Funktion in der skalierenden Variante. Da aber Main trotz der vier Implementationsvarianten kompakt bleiben soll, verzichten wir auf das dritte Kommandoargument zur Variantenauswahl und lassen Main die Berechnung durch alle vier Varianten zugleich ausführen. Programm 3.53 C#: ggT-Rechner, objektorientiert public class GcdClient { protected protected protected protected static static static static PairWithGcdR PairWithGcdR PairWithGcdV PairWithGcdV pairRR pairRI pairVR pairVI = = = = new new new new PairWithGcdRR(0, 0); PairWithGcdRI(0, 0); PairWithGcdVR(); PairWithGcdVI(); protected const string Hint = "Please call the gcd client with two integer arguments! "; public static void Write (string name, int p, int q, int gcd) { System.Console.WriteLine ("Gcd" + name + " (" + p + ", " + q + ") = " + gcd); } public static void Main (string[] args) { if (args.Length < 2) { System.Console.WriteLine("Missing Argument. " + Hint); } else { try { int p = System.Convert.ToInt32(args[0]); int q = System.Convert.ToInt32(args[1]); pairRR.Set(p, q); pairRI.Set(p, q); pairVR.Set(p, q); pairVI.Set(p, q); Write("RR", p, q, pairRR.Gcd()); Write("RI", p, q, pairRI.Gcd()); Write("VR", p, q, pairVR.Gcd()); Write("VI", p, q, pairVI.Gcd()); } catch (System.Exception e) { System.Console.WriteLine ("Wrong Argument. " + Hint + e.Message); } } } } pairRR, pairRI, pairVR Statische Referenz protected protected protected protected static static static static und pairVI sind durch PairWithGcdR PairWithGcdR PairWithGcdV PairWithGcdV pairRR pairRI pairVR pairVI = = = = new new new new PairWithGcdRR(0, 0); PairWithGcdRI(0, 0); PairWithGcdVR(); PairWithGcdVI(); als statische Referenzen definiert. Die Initialisierungsteile werden einmal beim Programmstart vor dem Aufruf von Main ausgeführt, dabei werden für pairRR, pairRI 27.9.12 Objekte dynamisch auf der Halde, © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 34 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt pairVR, pairVI Polymorphe Referenz 3.2.6 Wertvariablen direkt bei der Klasse GcdClient angelegt, auf die die Referenzen dann zeigen. Wie in der Java-Variante sind die Referenzen polymorph: Ihre statischen Typen sind die abstrakte Klasse PairWithGcdR bzw. das Interface PairWithGcdV, die dynamischen Typen sind durch die Konstruktoraufrufe festgelegt. Bei pairRR, pairRI werden programmiererdefinierte Konstruktoren aufgerufen, bei pairVR, pairVI Standardkonstruktoren. Beachten Sie die polymorphen Aufrufe in Main! Fazit Der Entwurf objektorientierter Lösungen des ggT-Problems von Bild 3.1 lässt sich in allen Auswahlsprachen ähnlich realisieren. Alle Sprachen kennen abstrakte Klassen. Redefinierbarkeit, Polymorphie und dynamisches Binden sind Standard (Component Pascal, Eiffel, Java), erfordern speziell markierte Methoden (C++, C#). Der Export von Merkmalen nur an Erweiterungsklassen ist aufwändig durch disziplinierte Zuordnung von Klassen zu Modulen bzw. Paketen (Component Pascal, Java), leicht durch ein spezielles Sprachkonstrukt (Eiffel, C++, C#) zu erreichen. Daten werden teilweise (Component Pascal), immer (Eiffel, C*) mit Standardwerten initialisiert. Die Initialisierung von Objekten obliegt der Verantwortung des Programmierers (Component Pascal), wird durch die Sprache erzwungen, und zwar durch Erzeugungsprozeduren (Eiffel), Konstruktoren (C*). Als Speicherarten gibt es einen globalen Datenspeicher, und zwar in statischer Form für statisch gebundene Programme (C++), dynamischer Form für dynamisch geladene Module bzw. Klassen (Component Pascal, Java, C#), einen Laufzeitkeller und eine Halde (Component Pascal, Eiffel, C*). Die Freigabe nicht mehr benötigten Haldenspeichers erfolgt manuell durch den Programmierer (C++), automatisch durch eine Speicherbereinigung (garbage collector) (Component Pascal, Eiffel, Java, C#). 3.2.7 Aufgaben Aufgabe 3.11 Ablaufverfolgung einer ggT-Berechnung Verfolgen Sie den Ablauf der Berechnung des größten gemeinsamen Teilers eines Paars mindestens zweistelliger Zahlen unter der (1) (2) (3) (4) (5) funktional-iterativen, funktional-rekursiven, objektorientiert-iterativen, objektorientiert-rekursiven Wertsemantik-, objektorientiert-rekursiven Referenz- oder Zeigersemantik- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 35 Implementation des euklidischen Algorithmus! Zeichnen Sie dazu die Objekte im Laufzeitkeller und auf der Halde auf! Abstrahieren Sie dabei von speziellen Sprachen! Sprachen: Alle, sofern sie die Implementation erlauben. Aufgabe 3.12 ggT- und kgV-Kunde (1) (2) Testen Sie die Zahlenpaarklassen und den ggT-Kunden nach 3.2! Ergänzen Sie die Zahlenpaarklassen um eine Funktion für das kleinste gemeinsame Vielfache (sie lässt sich als Schablonenmethode implementieren – wo?), den ggT-Kunden um die Ausgabe des kleinsten gemeinsamen Vielfachen der eingegebenen Zahlen! Sprachen: Alle. Aufgabe 3.13 Zahlenpaarklassen, effizienter Bei den Zahlenpaarklassen von 3.2 gilt: Auch wenn p, q ihre Werte behalten, berechnet die gcd-Funktion bei jedem Aufruf denselben Wert neu. Wenn p, q selten gesetzt und gcd oft abgefragt wird, ist dies ineffizient. Suchen Sie eine effizientere Lösung! Sprachen: Alle. Aufgabe 3.14 Stellenzahl, objektorientiert Entwickeln Sie für die Probleme aus Aufgabe 3.6 objektorientierte Lösungen! Aufgabe 3.15 Ziffernquadratsummenfolge Entwerfen Sie zu dem Ein-/Ausgabe-Problem Sprachen: Alle. Eingabe: Zwei positive ganze Zahlen m und n. Ausgabe: Eine Folge von n positiven ganzen Zahlen, von denen die erste gleich m ist und jede folgende gleich der Summe der Quadrate der Dezimalziffern der Vorgängerzahl. funktionale und objektorientierte Lösungen mit rekursiven und iterativen Algorithmen! Programmieren Sie Kommandozeilenrechner dazu, testen Sie das Programm und verbalisieren Sie das Geheimnis dieser Zahlenfolgen! Sprachen: Alle. 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden In 3.2 haben wir objektorientierte Lösungen des ggT-Problems studiert. Nun extrahieren wir daraus abstraktere Lösungen zu abstrakteren Problemen, die wir mit Konzeptklassen beschreiben und partiell als Schablonenmethoden (template method), einem objektorientierten Entwurfsmuster (design pattern) aus dem Katalog von Gamma u.a., implementieren [GHJV04]. Das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 5 wenden wir noch einmal auf höherem Abstraktionsniveau an. Bild 3.2 Abstraktes Problem mit abstrakter Lösung PROBLEM_SOLVER Anfangswerte einstellen Lösungswerte abfragen PROBLEM SOLUTION Lösungswerte einstellen 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 36 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Von den Konzeptklassen in Bild 3.2 fordern wir: Anforderungen (1) (2) (3) (4) PROBLEM_SOLVER kann ein PROBLEM-Objekt erzeugen und bei diesem beliebig oft Anfangswerte einstellen. PROBLEM hat ein SOLUTION-Objekt, das nur PROBLEM selbst erzeugen und bei dem nur PROBLEM die Lösungswerte einstellen kann. PROBLEM erzeugt sein SOLUTION-Objekt nur einmal und wiederverwendet es für Lösungen zu nacheinander eingestellten Anfangswerten. PROBLEM_SOLVER kann die Lösungswerte beim SOLUTION-Objekt seines PROBLEM-Objekts abfragen, aber nicht verändern. PROBLEM_SOLVER arbeitet Programm 3.54 Pseudocode: Problemlöser 3.3.1 nach dem Aktion-Abfrage-Schema etwa so: class PROBLEM_SOLVER root_action make, main is do problem : PROBLEM problem.set (start_values) solution_values := problem.solution.values end end Teile und herrsche – funktional Wir konzentrieren uns zunächst auf den PROBLEM-Teil und suchen eine funktionale Lösung. Zum ggT-Problem stellt Programm 3.1 eine endrekursive Lösung in funktionalem Pseudocode dar. Vom konkreten Problem abstrahierend betrachten wir eine allgemeine Kategorie von Problemen, für die eine ähnlich einfache endrekursive Lösung existiert. Das Lösungsverfahren ist ein Beispiel für Verfahren, die dem Teile-und-herrsche-Prinzip (divide and conquer) folgen, das sich verbal so beschreiben lässt: Teile-und-herrscheVerfahren Ist das Problem direkt lösbar, nimm die direkte Lösung als Lösung, sonst zerlege das Problem in Teilprobleme derselben Art und wende darauf das Lösungsverfahren rekursiv an. Genauer erfüllen die Probleme der betrachteten Kategorie DIVISIBLE_PROBLEM diese Bedingungen: Eigenschaften teilbarer Probleme (1) (2) (3) (4) Das Problem kann so einfach sein, dass es direkt lösbar ist. Ein nicht direkt lösbares Problem lässt sich auf ein einfacheres Teilproblem derselben Art reduzieren. Die Reduktion führt in endlich vielen Schritten zu einem direkt lösbaren Teilproblem. Die Lösung eines Teilproblems ist gleichzeitig Lösung des größeren Problems, aus dessen Reduktion es entstanden ist. Programm 3.55 formuliert das allgemeine Lösungsverfahren in eiffelischem funktionalem Pseudocode. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden Programm 3.55 Funktionaler Pseudocode: Endrekursiv gelöstes Problem 3 – 37 directly_solvable (problem : DIVISIBLE_PROBLEM) : BOOLEAN is abstract direct_solution (problem : DIVISIBLE_PROBLEM) : SOLUTION is precondition directly_solvable (problem) abstract part (problem : DIVISIBLE_PROBLEM) : DIVISIBLE_PROBLEM is precondition not directly_solvable (problem) abstract solution (problem : DIVISIBLE_PROBLEM) : SOLUTION is if directly_solvable (problem) then direct_solution (problem) else solution (part (problem)) end Wie lässt sich das konkrete ggT-Programm 3.1 aus dem daraus abstrahierten Programm 3.55 zurück konkretisieren? Durch Einsetzen der Teile der ursprünglichen Rekursionsformel (3.4) S. 2 ⎧p ggT(p, q) = ⎨ ⎩ggT (q, p mod q) falls q = 0 sonst in die entsprechenden Funktionen: Programm 3.56 Funktionaler Pseudocode: ggT als Konkretisierung GCD_PROBLEM extends DIVISIBLE_PROBLEM is INTEGER × INTEGER GCD_SOLUTION extends SOLUTION is NATURAL directly_solvable ((p, q) : GCD_PROBLEM) : BOOLEAN is q = 0 direct_solution ((p, q) : GCD_PROBLEM) : GCD_SOLUTION is abs (p) part ((p, q) : GCD_PROBLEM) : GCD_PROBLEM is (q, p mod q) solution ((p, q) : GCD_PROBLEM) : GCD_SOLUTION is inherited Die folgenden Umformungsschritte bieten sich an: Umformungsschritte (1) (2) (3) Transformiere die abstrakte funktionale Lösung Programm 3.55 in eine abstrakte objektorientierte Lösung. Benutze dazu Konzeptklassen und Schablonenmethoden für partielle Implementation. Transformiere in der abstrakten objektorientierten Lösung den endrekursiven Algorithmus in einen strukturierten iterativen Algorithmus. Konkretisiere die abstrakte objektorientierte Lösung zu einer Lösung des ggTProblems. Wir führen zu Schritt (1) in 3.3.2 einen sprachunabhängigen Entwurf aus, implementieren diesen in 3.3.3 in Eiffel und führen damit Schritt (2) aus, führen dann zu Schritt (3) in 3.3.4 wieder einen sprachunabhängigen Entwurf aus, den wir in 3.3.5 in Eiffel implementieren. Dann evaluieren wir das Erreichte und planen weitere Schritte. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 38 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt 3.3.2 Objektorientierter Entwurf der abstrakten Problemlösung nur mit Abfragen Bild 3.3 Konzeptklassen für rekursiv teilbares Problem nur mit Abfragen PROBLEM abstract generic query SOLUTION generic param. solution abstract DIVISIBLE_PROBLEM_Q abstract generic query directly_solvable direct_solution part DIVISIBLE_PROBLEM_QR abstract generic query abstract abstract abstract DIVISIBLE_PROBLEM_QI abstract generic solution -- template recursive query solution -- template iterative Der in Programm 3.55 erscheinende Typ DIVISIBLE_PROBLEM wird zu einer völlig abstrakten Konzeptklasse DIVISIBLE_PROBLEM_Q. Die Funktionen von Programm 3.55, die alle einen DIVISIBLE_PROBLEM-Parameter haben, werden zu abstrakten parameterlosen Abfragen von DIVISIBLE_PROBLEM_Q. Wir brauchen DIVISIBLE_PROBLEM_Q als Abstraktion, um die Abfrage solution in Nachfolgerklassen unterschiedlich implementieren zu können. In den Nachfolgerklassen DIVISIBLE_PROBLEM_QR und DIVISIBLE_PROBLEM_QI wird solution als Schablonenmethode mittels Aufrufen der abstrakten Abfragen directly_solvable, direct_solution, part implementiert, und zwar mit einem rekursiven bzw. iterativen Algorithmus. 3.3.3 Eiffel – Konzeptklassen aus Bild 3.3 wäre als Konzeptklasse leer, die Abfrage solution in PROBLEM wäre in konkreten Erweiterungsklassen kovariant an Erweiterungen von SOLUTION anzupassen, was sprachabhängig zu Komplikationen führen kann. Geschickter ist, SOLUTION als generischen Parameter der abstrakten generischen Klasse PROBLEM zu realisieren. SOLUTION Programm 3.57 Eiffel: Generische Konzeptklasse für Problem note description: "Concept class: Abstract problem that has a generic solution" deferred class PROBLEM [S] feature solution : S deferred ensure solution_exists: Result /= Void end end -- class PROBLEM Export Die abstrakte Klasse DIVISIBLE_PROBLEM_Q von Programm 3.58 exportiert die neuen Abfragen nur an sich selbst, denn Kunden wie PROBLEM_SOLVER brauchen sie nicht zu kennen. Warum an sich selbst? In Eiffel ist die Schutzeinheit das Objekt, nicht die Klasse. Wären die Abfragen durch feature {NONE} © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 39 ganz geschützt, dürfte ein Objekt nur sich selbst danach abfragen. Damit ließe sich nur die rekursive Implementationsvariante realisieren. Durch feature {DIVISIBLE_PROBLEM_Q} kann ein Objekt auch andere Objekte abfragen – diese Eigenschaft verlangt die iterative Implementationsvariante. Verankerter Typ Die Abfrage part soll ein Teilproblem derselben Art liefern, doch hier in der Konzeptklasse ist die konkrete Art noch unbekannt. Die Vereinbarung part : DIVISIBLE_PROBLEM_Q wäre zu schwach, da part im Nachfolger X von DIVISIBLE_PROBLEM_Q ein Objekt des Nachfolgers Y von DIVISIBLE_PROBLEM_Q liefern dürfte. Als Typ von part wird kein absoluter Typ benötigt, sondern ein relativer Typ, der sich mit der Vererbung an den Typ des aktuellen Objekts anpasst. Dies lässt sich mit dem Eiffel-Konstrukt des verankerten Typs (anchored type) ausdrücken. Durch die Vereinbarung part : like Current ist der Typ von part in allen Nachfolgerklassen gleich dem Typ des aktuellen Objekts Current. Programm 3.58 Eiffel: Generische Konzeptklasse für teilbares Problem mit Abfragen note description: "Concept class: Abstract divisible problem with query interface" deferred class DIVISIBLE_PROBLEM_Q [S] inherit PROBLEM [S] feature {DIVISIBLE_PROBLEM_Q} directly_solvable : BOOLEAN deferred end direct_solution : like solution require simple_case: directly_solvable deferred ensure dircect_solution_exists: Result /= Void end part : like Current require recursive_case: not directly_solvable deferred ensure part_exists: Result /= Void end end -- class DIVISIBLE_PROBLEM_Q 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 40 Programm 3.59 Eiffel: Generische Konzeptklasse für endrekursiv gelöstes Problem 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt note description: "Concept class: Divisible problem, query interface, recursive template" deferred class DIVISIBLE_PROBLEM_QR [S] inherit DIVISIBLE_PROBLEM_Q [S] feature solution : S -- Template query in tail-recursive form. do if directly_solvable then Result := direct_solution else Result := part.solution end end -- solution end -- class DIVISIBLE_PROBLEM_QR Den endrekursiven Algorithmus von solution in Programm 3.59 nach dem Umformungsschema von S. 5 in einen iterativen Algorithmus umzuformen, erfordert zwei Vorbereitungsschritte: Das aktuelle Objekt ist ein impliziter Parameter des rekursiven Aufrufs. Um ihn explizit zu machen, ist auf das aktuelle Objekt mit einer lokalen Referenz (z.B. namens problem) zuzugreifen. Eiffel kennt keine Sprunganweisungen; dynamische Enden von Routinen fallen stets mit ihrem statischen Ende zusammen. Die möglichen dynamischen Enden in den Zweigen der Auswahlanweisung sind zu markieren (z.B. mit return-Anweisungen als Kommentare, die später eliminiert werden). Programm 3.60 Eiffel: Schablonenmethode zu endrekursiv gelöstem Problem, umformungsbereit solution : S local problem : like Current do problem := Current if problem.directly_solvable then Result := problem.direct_solution -- return else problem := problem.part Result := problem.solution -- return end end -- solution Der rekursive Aufruf problem.solution hat die funktionale Form solution (problem) Da problem der einzige Parameter ist, können die Schritte (1) bis (3) des Umformungsschemas von S. 5 – also die Vereinbarung einer lokalen Variablen, das Speichern des Aktualparameters und das Rückspeichern in den Formalparameter – entfallen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden Programm 3.61 Pseudo-Eiffel: Schablonenmethode zu endrekursiv gelöstem Problem, umgeformt 3 – 41 solution : S local problem : like Current do problem := Current loop if problem.directly_solvable then Result := problem.direct_solution -- return else problem := problem.part Result := problem.solution -- return end end end -- solution Nun ist nur noch die unbedingte Schleife (die es in Eiffel nicht gibt) in die EiffelSchleife umzuformen und die Schablonenmethode in die Konzeptklasse DIVISIBLE_PROBLEM_QI einzubauen. Programm 3.62 Eiffel: Generische Konzeptklasse für iterativ gelöstes Problem note description: "Concept class: Divisible problem, query interface, iterative template" deferred class DIVISIBLE_PROBLEM_QI [S] inherit DIVISIBLE_PROBLEM_Q [S] feature solution : S -- Template query in iterative form. local problem : like Current do from problem := Current until problem.directly_solvable loop problem := problem.part end Result := problem.direct_solution end -- solution end -- class DIVISIBLE_PROBLEM_QI 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 42 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Bild 3.4 Konzept-, Schnittstellen- und Implementationsklassen für ggT-Problem nur mit Abfragen PROBLEM abstract generic query SOLUTION generic param. solution abstract DIVISIBLE_PROBLEM_Q abstract generic query directly_solvable direct_solution part abstract abstract abstract binden DIVISIBLE_PROBLEM_QR abstract generic query DIVISIBLE_PROBLEM_QI abstract generic query solution -- template recursive solution -- template iterative alternativ erben GCD_PROBLEM_Q 3.3.4 abstract GCD_SOLUTION query p q query gcd init_action make init_action make GCD_PROBLEM_Q1 GCD_PROBLEM_Q2 query query r s directly_solvable direct_solution part init_action make redefined directly_solvable direct_solution part Objektorientierter Entwurf der konkreten Problemlösung Wie lässt sich mit den Konzeptklassen die Lösung des ggT-Problems beschreiben? Zunächst sind die Schnittstellen um Dienste des konkreten Problems zu erweitern. Die so erhaltenen noch abstrakten Schnittstellenklassen lassen sich dann durch Implementationsklassen konkretisieren. Bild 3.4 enthält im oberen Teil Bild 3.3, um den Entwurf als Ganzes sichtbar zu machen. 3.3.5 Eiffel 3.3.5.1 Schnittstellenklassen Die ggT-Lösungsklasse ist so einfach, dass sie sofort ohne Variation implementiert wird. Sie ist zugleich Schnittstellen- und Implementationsklasse. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden Programm 3.63 Eiffel: Lösung zum ggT-Problem 3 – 43 note description: "Interface and implementation class: Solution of gcd problem" class GCD_SOLUTION create {GCD_PROBLEM_Q} make feature gcd : INTEGER feature {GCD_PROBLEM_Q} make (new_gcd : INTEGER) require argument_nonnegative: new_gcd >= 0 do gcd := new_gcd ensure argument_accepted: gcd = new_gcd end -- make invariant gcd_valid: gcd >= 0 end -- class GCD_SOLUTION Erfüllt GCD_SOLUTION die Anforderungen von S. 36? Die Exportbeschränkungen Anforderung (2) S. 36 Anforderung (3) S. 36 Anforderung (4) S. 36 create {GCD_PROBLEM_Q} make feature {GCD_PROBLEM_Q} make ... für make garantieren, dass nur Nachfolger von GCD_PROBLEM_Q Objekte von GCD_SOLUTION erzeugen und Lösungswerte new_gcd einstellen können. Da make nicht nur Erzeugungsprozedur, sondern auch als normale Routine benutzbar ist, können Nachfolger von GCD_PROBLEM_Q ihr einmalig erzeugtes GCD_SOLUTION-Objekt für nacheinander berechnete Lösungswerte wiederverwenden. Der Lösungswert ist durch feature gcd : INTEGER als öffentliches Attribut vereinbart, das kundenseitig nur lesbar, nicht veränderbar ist. Als ggT-Problemklassen definieren wir eine Abstraktion mit mehreren Konkretisierungen. Da das ggT-Problem durch die Zahlen p und q beschrieben ist, genügt es, für sie in der Abstraktion Abfragen und eine Prozedur make zum Setzen der Werte vorzusehen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 44 Programm 3.64 Eiffel: ggT-Problem, abstrakt 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt note description: "Interface class: Problem part of gcd problem, query interface" deferred class GCD_PROBLEM_Q inherit DIVISIBLE_PROBLEM_QI [GCD_SOLUTION] -- or DIVISIBLE_PROBLEM_QR feature p, q : INTEGER make (new_p, new_q : INTEGER) do p := new_p q := new_q ensure new_p_accepted: p = new_p new_q_accepted: q = new_q end -- make invariant gcd_bounded_by_p: p /= 0 implies solution.gcd <= p.abs gcd_bounded_by_q: q /= 0 implies solution.gcd <= q.abs end -- class GCD_PROBLEM_Q 3.3.5.2 Implementationsklassen Die erste Konkretisierung der Problemklasse erklärt make zur Erzeugungsprozedur und definiert die von DIVISIBLE_PROBLEM_Q geerbten abstrakten Abfragen direkt. Programm 3.65 Eiffel: ggT-Problem Variante 1 note description: "Implementation: Gcd problem, query interface, straightforward" class GCD_PROBLEM_Q1 inherit GCD_PROBLEM_Q create make feature {GCD_PROBLEM_Q1} directly_solvable : BOOLEAN do Result := q = 0 end direct_solution : like solution do create Result.make (p.abs) end part : like Current do create Result.make (q, p \\ q) end end -- class GCD_PROBLEM_Q1 ☺ Programm 3.65 bietet eine kompakte, elegante Problemlösung, von deren Korrektheit man sich leicht überzeugt, da die Teile der Rekursionsformel (3.4) S. 2 ⎧p ggT(p, q) = ⎨ ⎩ggT (q, p mod q) falls q = 0 sonst © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden Anforderung (1) S. 36 3 – 45 direkt zu erkennen sind. Es erfüllt die Anforderung (1) S. 36, da make nicht nur beim Erzeugen, sondern auch zum Reinitialisieren benutzbar ist. Aber leider ist Programm 3.65 ineffizient, weil part durch create Result.make (q, p \\ q) für jedes Teilproblem ein neues Objekt dynamisch erzeugt. Auch das von solution aufgerufene direct_solution erzeugt durch create Result.make (p.abs) Anforderung (3) S. 36 jedesmal ein neues Objekt, was der Anforderung (3) S. 36 widerspricht. Objekterzeugungen wollten wir durch die iterative Implementation gerade vermeiden. Verlieren wir etwa mit der Abstraktion zwangsläufig an Effizienz? Nein! Die erste Idee, die Problem- und Lösungsklassen zu expandieren, damit part, solution und direct_solution mit Wert- statt Referenzobjekten arbeiten, verwerfen wir allerdings weil Funktionen, die Wertobjekte zurückgeben, kaum effizient zu implementieren sind. Stattdessen versuchen wir, die Lösungselemente der iterativen Variante geeignet in die Implementationsklasse einzubringen, nämlich die Variablen r, s, t, mit denen die Berechnung durchzuführen ist. Programm 3.66 Eiffel: ggT-Problem Variante 2 note description: "Implementation: Gcd problem, query interface, hidden side-effects" class GCD_PROBLEM_Q2 inherit GCD_PROBLEM_Q redefine make end create make feature {NONE} r, s : INTEGER feature make (new_p, new_q : INTEGER) do Precursor (new_p, new_q) r := p.abs s := q.abs if direct_solution = Void then create direct_solution.make (0) end ensure then direct_solution_exists: direct_solution /= Void end -- make feature {GCD_PROBLEM_Q2} directly_solvable : BOOLEAN do if s = 0 then direct_solution.make (r) Result := True end end -- directly_solvable direct_solution : like solution 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 46 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt part : like Current local t : INTEGER do t := r \\ s r := s s := t Result := Current end -- part end -- class GCD_PROBLEM_Q2 Redefinition Der redefine-Abschnitt ist ein Unterabschnitt des inherit-Abschnitts. Hier sind die Namen der Merkmale aufzulisten, die in Vorgängern schon implementiert sind und in dieser Klasse eine neue Implementation erhalten. Bisher aufgeschobene Merkmale dürfen im redefine-Abschnitt nicht genannt werden. In Programm 3.66 werden die Berechnungsvariablen r, s als geschützte Attribute vereinbart, in make initialisiert, in part für einen Berechnungsschritt benutzt, und in directly_solvable ausgewertet. direct_solution wird als Attribut vereinbart, einmal in make erzeugt und provisorisch initialisiert, und von directly_solvable mit dem Lösungswert versehen. part muss für das Teilproblem kein neues Objekt erzeugen, sondern gibt nur das aktuelle Objekt zurück, das das Teilproblem in r, s selbst speichert. ☺ Nebeneffekt Programm 3.66 ist effizienter als Programm 3.65 und erfüllt alle vier Anforderungen von S. 36. Ineffizient bleibt es insofern, als solution das Ergebnis bei jedem Aufruf neu berechnet, auch wenn p und q ihre Werte behalten haben. Als strukturelle Schwäche werte ich, dass Programm 3.66 in den Abfragen directly_solvable, part und solution Nebeneffekte enthält, die bekanntlich verpönt sind: directly_solvable verändert den Zustandsteil des aktuellen Objekts, der mit direct_solution abzufragen ist. part verändert den Zustand des aktuellen Objekts durch Zuweisungen an die verborgenen Attribute r, s. solution bewirkt indirekt Nebeneffekte, weil es directly_solvable und part aufruft. Somit handelt es sich hier um versteckte Nebeneffekte, die kundenseitig nicht erkennbar sind, da direct_solution, r und s nur der Problemklasse GCD_PROBLEM_Q2 selbst zugänglich sind. Hinter der öffentlichen Schnittstelle verborgene Nebeneffekte sind weniger problematisch als öffentlich wahrnehmbare. Trotzdem ist Programm 3.66 wegen der Nebeneffekte weniger klar und schwerer verifizierbar als Programm 3.65. 3.3.6 Objektorientierter Entwurf mit Abfragen und Aktionen Deshalb suchen wir eine nebeneffektfreie, bessere Lösung – freilich wieder erst in einem sprachunabhängigen Entwurf. Für jede nebeneffektbehaftete Abfrage von Programm 3.66 definieren wir eine Aktion, die den entsprechenden Effekt bewirkt: check_solvability divide compute_solution zu zu zu directly_solvable, part, solution. directly_solvable lässt sich dann als Attribut fertig implementieren. part, das nur das aktuelle Objekt zurückgibt, entfällt ganz. solution bleibt, da es von der Konzeptklasse PROBLEM abstammt. Somit ist die neue Schnittstelle gegenüber der abfrageorientierten um einen Dienst breiter. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden Bild 3.5 Konzept- und Implementationsklassen für ggTProblem mit Abfragen und Aktionen PROBLEM abstract generic query 3 – 47 SOLUTION generic param. solution abstract DIVISIBLE_PROBLEM_QAI abstract generic query directly_solvable action check_solvability abstract divide abstract compute_solution -- template iterative binden GCD_PROBLEM_QAI p q r s solution query init_action action 3.3.7 make check_solvability divide GCD_SOLUTION query gcd init_action make Eiffel Da in Eiffel Aktionen Kommandos heißen, verwenden wir statt QAI die Abkürzung QCI. 3.3.7.1 Programm 3.67 Eiffel: Konzeptklasse für iterativ-imperativ gelöstes Problem Konzeptklasse note description: "Concept class: Divisible problem, query-command interface, iterative" deferred class DIVISIBLE_PROBLEM_QCI [S] inherit PROBLEM [S] feature {NONE} directly_solvable : BOOLEAN check_solvability -- Update directly_solvable. deferred end divide require recursive_case: not directly_solvable deferred end 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 48 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt compute_solution -- Template command. do from check_solvability until directly_solvable loop divide check_solvability end end -- compute_solution end -- class DIVISIBLE_PROBLEM_QCI Da bei dieser iterativen Variante alle Merkmale nur noch vom aktuellen Objekt gebraucht werden, schränken wir ihren Exportstatus ein. 3.3.7.2 Implementationsklassen In der Lösungsklasse GCD_SOLUTION Programm 3.63 ist nur GCD_PROBLEM_QCI in die Kundenliste von make aufzunehmen. Programm 3.68 Eiffel: Lösung zum ggT-Problem, erweitert class GCD_SOLUTION -- Stuff not shown same as in Programm 3.63. create {GCD_PROBLEM_Q, GCD_PROBLEM_QCI} make feature {GCD_PROBLEM_Q, GCD_PROBLEM_QCI} -- Stuff not shown same as in Programm 3.63. end -- class GCD_SOLUTION Programm 3.69 Eiffel: ggT-Problem Variante 3 note description: "Implementation: Gcd problem, query-command interface, iterative" class GCD_PROBLEM_QCI inherit DIVISIBLE_PROBLEM_QCI [GCD_SOLUTION] redefine solution end create make feature p, q : INTEGER © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 49 make (new_p, new_q : INTEGER) do p := new_p q := new_q r := p.abs s := q.abs directly_solvable := False if solution = Void then create solution.make (0) end compute_solution ensure new_p_accepted: p = new_p new_q_accepted: q = new_q solution_exists: solution /= Void end -- make solution : GCD_SOLUTION feature {NONE} r, s : INTEGER check_solvability do if s = 0 then directly_solvable := True solution.make (r) end end -- check_solvability divide local t : INTEGER do t := r \\ s r := s s := t end -- divide end -- class GCD_PROBLEM_QCI Evaluation Programm 3.69 ☺ ist effizienter als Programm 3.66, ☺ erfüllt alle vier Anforderungen von S. 36, ☺ solution liefert ein gespeichertes Ergebnis, das nur dann von make neu berechnet wird, wenn p und q neue Werte erhalten, ☺ kommt ohne die versteckten Nebeneffekte von Programm 3.66 aus, gewinnt gegenüber Programm 3.66 an Klarheit, erreicht jedoch nicht die Eleganz von Programm 3.65. 3.3.7.3 Programm 3.70 Eiffel: ggT-Rechner als Problemlöser Kundenklasse note description: "Command-line calculator for greatest common divisor of integer pairs" class GCD_PROBLEM_SOLVER create make 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 50 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt feature {NONE} gcd_q1 : GCD_PROBLEM_Q once create {GCD_PROBLEM_Q1} Result.make (0, 0) ensure gcd_q1_exists: Result /= Void end gcd_q2 : GCD_PROBLEM_Q once create {GCD_PROBLEM_Q2} Result.make (0, 0) ensure gcd_q2_exists: Result /= Void end gcd_qci : GCD_PROBLEM_QCI once create Result.make (0, 0) ensure gcd_qci_exists: Result /= Void end write (name : STRING; p, q, gcd : INTEGER) do io.put_string ("gcd_" + name + " (" + p.out + ", " + q.out + ") = " + gcd.out) io.put_new_line end -- write make (arguments : ARRAY [STRING]) -- Given integer pairs as command arguments, print their greatest common divisor, -- computed by 3 variants. local i, p, q : INTEGER do if arguments = Void or else arguments.count < 3 then io.put_string ("Please call the gcd problem solver with integer pairs as arguments!") io.put_new_line else from i := 2 until arguments.count <= i or else not arguments.item (i - 1).is_integer or else not arguments.item (i).is_integer loop p := arguments.item (i - 1).to_integer q := arguments.item (i).to_integer gcd_q1.make (p, q) gcd_q2.make (p, q) gcd_qci.make (p, q) write ("q1 ", gcd_q1.p, gcd_q1.q, gcd_q1.solution.gcd) write ("q2 ", gcd_q2.p, gcd_q2.q, gcd_q2.solution.gcd) write ("qci", gcd_qci.p, gcd_qci.q, gcd_qci.solution.gcd) io.put_new_line i := i + 2 end end end -- make end -- class GCD_PROBLEM_SOLVER © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3.3.8 3 – 51 C++ Für die drei C*-Sprachen folgt nur die Umsetzung des letzten Entwurfs 3.3.6.; die Entwürfe 3.3.2 und 3.3.4 setzen Sie als Übung um (s. Aufgabe 3.16). Für die C*-Implementationsvarianten gilt dieselbe Bewertung wie für die Eiffel-Variante auf S. 49, sodass im Folgenden Bemerkungen zu sprachlichen Besonderheiten genügen. 3.3.8.1 Konzeptklassen Die Lösungsklasse realisieren wir auch in C++ als generischen Parameter S der abstrakten generischen Problemklasse Problem: Generische Klasse template <class S> class Problem C++ bezeichnet Generizität als Schablone (template); den Begriff sollte man nicht mit dem objektorientierten Entwurfsmuster Schablonenmethode (template method) verwechseln! Übrigens kennt C++ als hybride Sprache nicht nur generische Klassen, sondern auch generische Funktionen. Programm 3.71 C++: Generische Konzeptklasse für Problem // Header file: Problem.hpp #ifndef PROBLEM_HPP_ #define PROBLEM_HPP_ template <class S> class Problem { public: virtual S & solution () const = 0; }; #endif // PROBLEM_HPP_ Während Eiffel bei abstrakten parameterlosen Abfragen offen lässt, ob sie später als Attribute oder Funktionen zu implementieren sind, müssen sie in den C*-Sprachen als abstrakte Funktionen deklariert werden. Da aber Attribute nie öffentlich sein, sondern ggf. durch öffentliche Lesezugriffsfunktionen ergänzt werden sollen, liegt darin kein zusätzlicher Nachteil. Programm 3.72 C++: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem, Schnittstelle // Header file: DivisibleProblemQAI.hpp #ifndef DIVISIBLE_PROBLEM_QAI #define DIVISIBLE_PROBLEM_QAI #include "Problem.hpp" template <class S> class DivisibleProblemQAI : public Problem<S> { protected: bool directly_solvable; virtual void check_solvability () = 0; virtual void divide () = 0; // Precondition: !directly_solvable virtual void compute_solution (); }; template <class S> void DivisibleProblemQAI<S>::compute_solution () { while (true) { check_solvability(); if (directly_solvable) break; divide(); } } #endif // DIVISIBLE_PROBLEM_QAI 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 52 Implementation in der Schnittstelle 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Normalerweise enthält eine Schnittstellendatei nur die Deklarationen der exportierten Klassen (Funktionen). Doch bei generischen Klassen (Funktionen) muss die Schnittstellendatei auch die Implementationen der Klassen (Funktionen) vollständig enthalten, denn nur so kann der Kompilierer an inkludierenden Stellen Code für Konkretisierungen der Klassen (Funktionen) erzeugen. Zwischen den Klassenklammern steht nur die Schnittstelle der Klasse. Implementationen von Klassen- und Elementfunktionen stehen außerhalb davon, normalerweise in der zugehörigen Implementationsdatei, aber bei generischen Klassen folgen sie in der Schnittstellendatei nach dem Klassentext. Der iterative Algorithmus von compute_solution lässt sich mit einer Bedingungsschleife oder einer unbedingten Schleife mit bedingtem Sprung aus dem Schleifenrumpf formulieren. Es handelt sich um eine „n+1/2-Schleife“ (Dijkstra). Analysieren wir beide Varianten: Bedingungsschleife Evaluation check_solvability(); while (!directly_solvable) { divide(); check_solvability(); } ☺ Streng strukturierte Schleife, da ein Eingang, ein Ausgang; mit Struktogramm dar- stellbar. ☺ Schachtelungstiefe der Anweisungen: 2. Aufruf von check_solvability an zwei Stellen. Unbedingte Schleife mit Sprung aus dem Rumpf Evaluation Fazit 3.3.8.2 while (true) { check_solvability(); if (directly_solvable) { break; } else { divide(); } } for (;;) { check_solvability(); if (directly_solvable) break; divide(); } Schwach strukturierte Schleife, da sie aus der Rumpfmitte verlassen wird; hat zwar nur je einen Ein- und Ausgang, ist aber nicht mit einem Struktogramm darstellbar. Schachtelungstiefe der Anweisungen: 3 wegen zusätzlichem if. while- oder for-Schleife erforderlich, da die C*-Sprachen kein Konstrukt für unbedingte Schleifen haben. Hoffentlich setzt der Kompilierer diese Pseudo-Bedingungsschleife so um, dass der Ausdruck true nicht zur Laufzeit ausgewertet wird! ☺ Aufruf von check_solvability an nur einer Stelle. Einziger Nachteil der Bedingungsschleife ist die Kopie einer Anweisung im Initialisierungsteil; einziger Vorteil der unbedingten Schleife ist, dass diese Anweisung nur einmal hinzuschreiben ist und nur einmal dafür Code erzeugt (d.h. ein Maschinenbefehl gespart ;-) wird. Welche Variante ist vorzuziehen? Das hängt davon ab, wie man die Vor- und Nachteile gewichtet. Da hier einer der raren Fälle vorliegt, in denen der Einsatz der break-Anweisung überhaupt einen Vorteil und nicht nur Nachteile bietet, wähle ich die unbedingte Schleife mit dem bedingtem Sprung in einer Zeile. Implementationsklassen Die ggT-Lösungsklasse GcdSolution bietet als Behälter der Lösungszahl gcd nichts Spektakuläres. Als Alternative zum scheußlichen get_-Präfix bei der Getter-Funktion verwende ich hier die Konvention, die Lesezugriffsfunktion passend zu benennen und das zugehörige Attribut genauso, aber mit dem hässlichen Präfix „_“ versehen. Die Anforderung (2) S. 36, dass nur Problem-Klassen Solution-Objekte erzeugen und Lösungswerte einstellen können, ist in C++ nicht erfüllbar, da dieser Schutzzustand © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 53 zwischen public und protected liegt. Einen Konstruktor und die set-Methode mit protected zu schützen wäre zu stark, weil sie dann nur für Ableitungsklassen von GcdSolution aufrufbar wären. Also muss set public sein; als Konstruktor genügt der öffentliche Standardkonstruktor. Damit kann aber jedes Programmteil GcdSolution-Objekte vereinbaren, erzeugen und ihren Zustand ändern. Programm 3.73 C++: Lösung zum ggTProblem, Schnittstelle // Header file: GcdSolution.hpp #ifndef GCD_SOLUTION_HPP_ #define GCD_SOLUTION_HPP_ class GcdSolution { protected: int _gcd; public: int gcd () const {return _gcd;} virtual void set (int new_gcd); }; #endif // GCD_SOLUTION_HPP_ Programm 3.74 C++: Lösung zum ggTProblem, Implementation // Implementation file: GcdSolution.cpp #include "GcdSolution.hpp" #include <cassert> void GcdSolution::set (int new_gcd) { assert(new_gcd >= 0); // precondition _gcd = new_gcd; } Die ggT-Problemklasse GcdProblemQAI liefert bei solution Anschauungsmaterial für das Zusammenspiel von Zeigern und Referenzen in C++. Programm 3.75 C++: ggT-Problem, Schnittstelle // Header file: GcdProblemQAI.hpp #ifndef GCD_PROBLEM_QAI #define GCD_PROBLEM_QAI #include "DivisibleProblemQAI.hpp" #include "GcdSolution.hpp" class GcdProblemQAI : public DivisibleProblemQAI<GcdSolution> { protected: GcdSolution * _solution; int _p, _q, r, s; void check_solvability (); void divide (); public: GcdSolution & solution () const {return *_solution;} int p () const {return _p;} int q () const {return _q;} virtual void set (int new_p, int new_q); GcdProblemQAI (); virtual ~GcdProblemQAI (); }; #endif // GCD_PROBLEM_QAI 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 54 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt Die doppelte Aufgabe, die make in Programm 3.69 als Erzeugungs- und Reinitialisierungsprozedur übernimmt, ist in den C*-Sprachen auf einen Konstruktor und eine Setter-Prozedur aufzuteilen. Im Konstruktor wird das GcdSolution-Objekt dynamisch auf der Halde erzeugt. Manuelle Speicherverwaltung Programm 3.76 C++: ggT-Problem, Implementation Da C++ keine automatische Speicherbereinigung hat, muss der Programmierer das Löschen dynamisch erzeugter Bestandteile von Objekten selbst programmieren. Deshalb ist hier ein Destruktor ~GcdProblemQAI zu definieren, der das GcdSolution-Objekt löscht, bevor das aktuelle GcdProblemQAI-Objekt gelöscht wird. Destruktoren sollen stets virtual deklariert sein, damit ihr Aufruf in polymorphen Situationen nicht unterbleibt. // Implementation file: GcdProblemQAI.cpp #include "GcdProblemQAI.hpp" #include <cassert> #include <cstdlib> void GcdProblemQAI::check_solvability () { if (s == 0) { directly_solvable = true; _solution->set(r); } } void GcdProblemQAI::divide () { assert(!directly_solvable); // precondition int t = r % s; r = s; s = t; } void GcdProblemQAI::set (int new_p, int new_q) { _p = new_p; _q = new_q; r = abs(_p); s = abs(_q); directly_solvable = false; compute_solution(); } GcdProblemQAI::GcdProblemQAI () { _solution = new GcdSolution(); } GcdProblemQAI::~GcdProblemQAI () { delete _solution; } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3.3.8.3 Programm 3.77 C++: ggT-Rechner als Problemlöser 3 – 55 Kundenfunktion // Implementation file: GcdProblemSolver.cpp #include "GcdProblemQAI.hpp" #include <cstdlib> #include <iostream> int main (int argc, char * argv[]) { if (argc < 3) { std::cout << "Please call the gcd problem solver with integer pairs as arguments!"; << std::endl; } else { GcdProblemQAI * gcd_problem = new GcdProblemQAI(); int p, q; for (int i = 2; i < argc; i += 2) { p = std::atoi(argv[i-1]); q = std::atoi(argv[i]); gcd_problem->set(p, q); std::cout << "gcd(" << gcd_problem->p() << ", " << gcd_problem->q() << ") = " << gcd_problem->solution().gcd() << std::endl; } delete gcd_problem; } return 0; } 3.3.9 Java Das Bedürfnis, den Programmieraufwand zu beschränken (also Faulheit), verführt uns dazu, das protected-Problem zu verdrängen, die eben postulierte Leitlinie 3.3 abzuschwächen, und alle Klassen des Entwurfs von Bild 3.5 in ein einziges Paket Problems zu legen. Diese Disziplinlosigkeit hat ihre gute Seite, denn sie ermöglicht, die Anforderung (2) S. 36 näherungsweise zu erfüllen. 3.3.9.1 Konzeptklassen In Java sind Interfaces (interface) wie rein abstrakte Klassen, die keinerlei Implementationsteile enthalten, also weder Attribute, noch Methodenrümpfe, noch Konstruktoren. Interfaces erlauben eine eingeschränkte Form von Mehrfachvererbung, da eine Klasse nur eine andere Klasse beerben, aber beliebig viele Interfaces implementieren darf. Interfaces können auch generisch sein. Da die Konzeptklasse Problem keine Implementationsteile enthält, bietet es sich an, sie in Java als Interface zu realisieren. Programm 3.78 Java: Generisches Konzept-Interface für Problem package Problems; // Only to be used for extensions of Problem and Solution. public interface Problem<S> { S solution (); } DivisibleProblemQAI kann kein Interface sein, da es mit directlySolvable ein Datenelement und mit computeSolution eine fertig implementierte Methode enthält. Da directlySolvable in Implementationsklassen schreibbar sein muss, muss es protected sein. Für Kundenklassen sollte es aber schreibgeschützt sein! Da checkSolvability und divide in Implementationsklassen definierbar sein müssen, müssen sie protected sein. Für Kundenklassen sollten sie aber aufrufgeschützt sein! 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 56 Programm 3.79 Java: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt package Problems; // Only to be used for extensions of Problem and Solution. public abstract class DivisibleProblemQAI<S> implements Problem<S> { protected boolean directlySolvable; protected abstract void checkSolvability (); protected abstract void divide (); // Precondition: !directlySolvable protected void computeSolution () { while (true) { checkSolvability(); if (directlySolvable) break; divide(); } } } 3.3.9.2 Programm 3.80 Java: Lösung zum ggTProblem Implementationsklassen package Problems; // Only to be used for extensions of Problem and Solution. public class GcdSolution { private int gcd; public int gcd () {return gcd;} protected void set (int gcd) { assert gcd >= 0 : "precondition"; this.gcd = gcd; } protected GcdSolution () {} } Der protected Konstruktor und die protected set-Methode garantieren, dass nur Klassen im Paket Problems GcdSolution-Objekte erzeugen und Lösungswerte einstellen können. Das entspricht fast der Anforderung (2) S. 36. Programm 3.81 Java: ggT-Problem package Problems; // Only to be used for extensions of Problem and Solution. public class GcdProblemQAI extends DivisibleProblemQAI<GcdSolution> { private GcdSolution solution; private int p, q, r, s; protected void checkSolvability () { if (s == 0) { directlySolvable = true; solution.set(r); } } protected void divide () { assert !directlySolvable : "precondition"; int t = r % s; r = s; s = t; } public GcdSolution solution () {return solution;} public int p () {return p;} public int q () {return q;} © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 57 public void set (int p, int q) { this.p = p; this.q = q; r = Math.abs(p); s = Math.abs(q); directlySolvable = false; computeSolution(); } public GcdProblemQAI () { solution = new GcdSolution(); } } 3.3.9.3 Programm 3.82 Java: ggT-Rechner als Problemlöser Kundenklasse class GcdProblemSolver { protected static final String HINT = "Please call the gcd problem solver" + "with integer pairs as arguments! "; public static void main (String[] args) { if (args.length < 2) { System.out.println("Missing Argument. " + HINT); } else { try { GcdProblemQAI gcdProblem = new GcdProblemQAI(); int p, q; for (int i = 1; i < args.length; i += 2) { p = Integer.parseInt(args[i-1]); q = Integer.parseInt(args[i]); gcdProblem.set(p, q); System.out.println ("gcd(" + gcdProblem.p() + ", " + gcdProblem.q() + ") = " + gcdProblem.solution().gcd()); } } catch (Exception e) { System.out.println("Wrong Argument. " + HINT + e.toString()); } } } } 3.3.10 C# 3.3.10.1 Konzeptklassen Programm 3.83 C#: Generische Konzeptklasse für Problem abstract class Problem<S> { public abstract S Solution (); } C# ist bei Interfaces strenger als Java: Da wir Solution in der direkten Ableitungsklasse DivisibleProblemQAI noch nicht implementieren wollen, kann Problem kein Interface, sondern muss eine abstrakte Klasse sein. Solution muss eine abstrakte Funktion sein, da Properties nicht abstrakt sein können. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 58 Programm 3.84 C#: Generische Konzeptklasse für iterativ-imperativ gelöstes Problem 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt abstract class DivisibleProblemQAI<S> : Problem<S> { protected bool directlySolvable; protected abstract void CheckSolvability (); protected abstract void Divide (); // Precondition: !directlySolvable protected virtual void ComputeSolution () { while (true) { CheckSolvability(); if (directlySolvable) break; Divide(); } } } 3.3.10.2 Programm 3.85 C#: Lösung zum ggTProblem Implementationsklassen class GcdSolution { protected int gcd; public int Gcd {get {return gcd;}} protected internal void Set (int gcd) { System.Diagnostics.Debug.Assert (gcd >= 0, "precondition"); this.gcd = gcd; } protected internal GcdSolution () {} } Die Schutzmodifikatoren protected internal schränken die Kundenmenge der Setter-Methode und des Konstruktors so weit möglich ein, nämlich auf Ableitungsklassen, die in derselben „assembly“ oder „dynamic link library“ wie die Basisklasse liegen. Programm 3.86 C#: ggT-Problem class GcdProblemQAI : DivisibleProblemQAI<GcdSolution> { protected GcdSolution solution; protected int p, q, r, s; protected override void CheckSolvability () { if (s == 0) { directlySolvable = true; solution.Set(r); } } protected override void Divide () { System.Diagnostics.Debug.Assert (!directlySolvable, "precondition"); int t = r % s; r = s; s = t; } public override GcdSolution Solution () {return solution;} public int P {get {return p;}} public int Q {get {return q;}} © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 3.3 Abstraktion mit Konzeptklassen und Schablonenmethoden 3 – 59 public virtual void Set (int p, int q) { this.p = p; this.q = q; r = System.Math.Abs(p); s = System.Math.Abs(q); directlySolvable = false; ComputeSolution(); } public GcdProblemQAI () { solution = new GcdSolution(); } } 3.3.10.3 Programm 3.87 C#: ggT-Rechner als Problemlöser Kundenklasse class GcdProblemSolver { protected const string Hint = "Please call the gcd problem solver with integer pairs as arguments! "; public static void Main (string[] args) { if (args.Length < 2) { System.Console.WriteLine("Missing Argument. " + Hint); } else { try { GcdProblemQAI gcdProblem = new GcdProblemQAI(); int p, q; for (int i = 1; i < args.Length; i += 2) { p = System.Convert.ToInt32(args[i-1]); q = System.Convert.ToInt32(args[i]); gcdProblem.Set(p, q); System.Console.WriteLine ("gcd(" + gcdProblem.P + ", " + gcdProblem.Q + ") = " + gcdProblem.Solution().Gcd); } } catch (System.Exception e) { System.Console.WriteLine ("Wrong Argument. " + Hint + e.Message); } } } } 3.3.11 Fazit Die Entwürfe 3.3.2, 3.3.4 und 3.3.6 abstrakter und konkretisierter Problemlösungen mit Konzeptklassen und Schablonenmethoden lassen sich in allen Auswahlsprachen ähnlich realisieren. Die Sprachvarianten unterscheiden sich nur in Details. Konzeptklassen realisiert man als abstrakte Klassen (Component Pascal, Eiffel, C*), Interfaces, wenn sie rein abstrakt sind (Java, C#), Die Problemklasse lässt sich in Eiffel und C* vorteilhaft generisch definieren. Den iterativen Algorithmus der Schablonenmethode implementiert man nur rein strukturiert (Eiffel), oder auch mit einem bedingten Sprung aus einer unbedingten Schleife (Component Pascal, C*). Das Erzeugen von Objekten von Lösungsklassen lässt sich 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 3 – 60 3 Funktional, rekursiv, iterativ, objektorientiert, abstrakt auf Problemklassen einschränken (Component Pascal, Eiffel), nicht auf Problemklassen einschränken (C*). Die Aufgaben Initialisierung und Reinitialisierung lassen sich mit einer einzigen Prozedur erledigen (Component Pascal, Eiffel), sind auf einen Konstruktor und eine Setter-Prozedur zu verteilen (C*). 3.3.12 Aufgaben Aufgabe 3.16 Entwürfe 3.3.2, 3.3.4 Implementieren Sie die Entwürfe 3.3.2 und 3.3.4 in C++, Java und C#! Aufgabe 3.17 ggT- und kgVProblemlöser Sprachen: C*. (1) (2) Testen Sie alle Klassen und den ggT-Problemlöser nach 3.3! Ergänzen Sie die Lösungsklasse um eine Abfrage für das kleinste gemeinsame Vielfache, die Problemklasse um die Berechnung des kgV, den ggT-Problemlöser um die Ausgabe des kgV der eingegebenen Zahlen! Sprachen: Alle. Aufgabe 3.18 Stellenzahl, konkretisierte Abstraktion Entwickeln Sie für die Probleme aus Aufgabe 3.6 und Aufgabe 3.14 Lösungen, die die abstrakten Lösungen aus 3.3 konkretisieren! Sprachen: Alle. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4 Reihungen und Abstraktionen Aufgabe Beispiel 4 Easter Bild 4 4 1961, (4) aLeitlinie 4on ALGOL 4 60 wasTabelle 4 in Brighton, England, with Around courseProgramm offered Peter Naur, Edsger W. Dijkstra, and Peter Landin as tutors. [...] It was there that I first learned about recursive procedures and saw how to program the sorting method which I had earlier found such difficulty in explaining. It was there that I wrote the procedure, immodestly named Quicksort, on which my career as a computer scientist is founded. C. A. R. Hoare1 Dieses Kapitel befasst sich in Programmbeispielen mit Reihungen und wichtigen Suchund Sortieralgorithmen. Dabei nutzt es die Abstraktionskonzepte Generizität und Vererbung und zeigt, wie man die Trennung von Anwendungen, wiederverwendbaren Testtreibern und funktionalen Einheiten mit Entwurfsmustern wie der Schablonenmethode erzielen kann. 4.1 Binäres Suchen in einer sortierten Reihung Bei den folgenden Problemen kommen Reihungen ins Spiel. Das erste Problem ist das Prüfen, ob eine Reihung sortiert ist. Problem Sortiertprüfung Gegeben: Eine Reihung array von Elementen mit einer Vollordnungsrelation ≤. Gesucht: Ist die Reihung sortiert, d.h. gilt array[first] ≤ ... ≤ array[last]? Mathematisch handelt es sich wie bei der ggT-Funktion um eine Abbildung, etwa sorted : Gn → lB. Informatisch handelt es sich um einen Spezialfall des Suchproblems, das der Krimskrams-Beitrag Folgen durchlaufen untersucht. Die passende Lösung ist eine boolesche Funktion mit der Reihung als Parameter, implementiert mit einem rekursiven oder iterativen Algorithmus. Das zweite Problem ist das Suchen eines Elements in einer sortierten Reihung. Problem Suchen Gegeben: Eine sortierte Reihung array von Elementen mit einer Vollordnungsrelation und ein mögliches Element item. Gesucht: (a) Ist das Element item in der Reihung enthalten, d.h. gibt es einen Index i mit array[i] = item? (b) Falls das Element item in der Reihung enthalten ist, ein Index i mit array[i] = item, sonst not_found. Mathematisch handelt es sich wieder um Abbildungen, etwa has : Gn × G → lB bzw. Lösungen index : Gn × G → {1,.., n, not_found}. Algorithmische Standardlösungen sind lineares und binäres Suchen. Lineares Suchen geht inkrementell vor: Durchlaufe die Reihung elementweise vom Anfang bis das gesuchte Element gefunden oder das Ende erreicht ist. Jeder Schritt 1 C. A. R. Hoare: The Emperor’s Old Clothes. ACM Turing Award Lecture 1980. Communications of the ACM, Vol. 24, No. 2 (February 1981) S. 75-83 © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 4 – Seite 1 von 46 4–2 Komplexität 4.1.1 4 Reihungen und Abstraktionen verkleinert den Suchraum um ein Element. Lineares Suchen behandelt der Krimskrams-Beitrag Folgen durchlaufen. Binäres Suchen setzt eine sortierte Reihung voraus und nutzt das Teile-und-herrsche-Prinzip: Vergleiche das gesuchte Element mit dem mittleren Element der Reihung. Ist das gesuchte Element kleiner, suche in der linken Hälfte, ist es größer, suche in der rechten Hälfte weiter, bis es gefunden oder der Suchraum leer ist. Jeder Schritt halbiert den Suchraum. Binäres Suchen behandelt dieser Abschnitt. Bei sortierter Reihung ist das effizientere binäre Suchen mit einem Laufzeitaufwand der Größenordnung O(log(n)) dem linearen Suchen mit einem Laufzeitaufwand der Größenordnung O(n) vorzuziehen. Die passende Lösung ist eine Funktion mit der sortierten Reihung und dem gesuchten Element als Parameter, implementiert mit einem rekursiven oder iterativen Algorithmus. Trennen wiederverwendbarer Komponenten von Anwendungen Wir haben im Beispiel 3.1 S. 3-1 die ggT-Funktion und den ggT-Rechner einfach zusammen in eine Komponente – je nach Sprache ein Modul oder eine Klasse – gepackt, aber schon in den Beispielen 3.2 S. 3-17 und 3.3 S. 3-35 die ggT-Klassen und den ggT-Rechner getrennt in zwei Komponenten realisiert. Nun fordern wir generell die professionell saubere modulare Trennung zwischen wiederverwendbaren Problemlösungen und ihren Anwendungen oder Testtreibern. Um die beiden Funktionen sorted und has möglichst wiederverwendbar zu machen, vereinbaren wir sie als Schnittstellenfunktionen einer geeigneten wiederverwendbaren Werkzeugkastenkomponente. Bild 4.1 Modulares Trennen verschiedener Aspekte 4.1.2 Anwendung Testtreiber Werkzeugkasten Prüfling Routinen schachteln oder nicht? Bei rekursiven Varianten von Routinen stellt sich diese Entwurfsfrage: Die rekursive Routine muss die sich von Aufruf zu Aufruf ändernden, aber auch die invarianten Größen kennen – woher? In Sprachen, die wie Component Pascal das Schachteln von Routinen erlauben, empfiehlt sich, die rekursive Routine in die Schnittstellenroutine zu schachteln und jede Routine mit den passenden Parametern zu versehen. In Sprachen, die wie Eiffel und C* kein Schachteln von Routinen erlauben, muss die Schnittstellenroutine ihre Parameter an die neben ihr stehende rekursive Routine weiterreichen – entweder über globale Größen oder per Parameterübergabe. Umformungsschemas zum Entschachteln geschachtelter Routinen beschreibt der Krimskrams-Beitrag Geschachtelte Routinen entschachteln. 4.1.3 Component Pascal Werkzeugkasten Für die Problemlösungen in Component Pascal sehen wir ein Werkzeugkastenmodul I3ToolsetForArrayOfInteger vor, das wir später erweitern. Der Moduldateiname beginnt mit Toolset um anzudeuten, dass das Modul neben Sorted und Has weitere Werkzeuge enthalten kann. Das zu I3ToolsetForArrayOfInteger gehörende Testtreibermodul I3TesterOfTSAI zeigen wir hier nicht. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung Programm 4.1 Component Pascal: Sortiertprüfung und binäres Suchen, rekursiv 4–3 MODULE I3ToolsetForArrayOfInteger; IMPORT BEC := BasisErrorConstants; TYPE Integer* Real* Numeric* Comparable* Any* = INTEGER; = REAL; = Integer; = Numeric; = Comparable; Index* = INTEGER; (* Right hand type may be substituted by *) (* SHORTINT, LONGINT *) (* SHORTREAL *) (* Real *) (* CHAR, ARRAY OF CHAR *) (* any type *) PROCEDURE SortedBetween* (IN array : ARRAY OF Comparable; left, right : Index) : BOOLEAN; (*! Postcondition: result = the elements of array with indices from left to right are ordered. !*) BEGIN ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondParsConsistent); RETURN (left >= right) OR ((array [left] <= array [left + 1]) & SortedBetween (array, left + 1, right)); END SortedBetween; PROCEDURE Sorted* (IN array : ARRAY OF Comparable) : BOOLEAN; (*! Postcondition: result = the elements of array are ordered. !*) BEGIN RETURN SortedBetween (array, 0, LEN (array) - 1); END Sorted; PROCEDURE Has* (IN array : ARRAY OF Comparable; item : Comparable) : BOOLEAN; (*! Postcondition: result = array contains item. !*) PROCEDURE HasBetween (left, right : Index) : BOOLEAN; VAR mid : Index; BEGIN ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondition); ASSERT (SortedBetween (array, left, right), BEC.precondition); IF left > right THEN RETURN FALSE; ELSE mid := (left + right) DIV 2; IF item < array [mid] THEN RETURN HasBetween (left, mid - 1); ELSIF item > array [mid] THEN RETURN HasBetween (mid + 1, right); ELSE RETURN TRUE; END; END; END HasBetween; BEGIN ASSERT (Sorted (array), BEC.precondition); RETURN HasBetween (0, LEN (array) - 1); END Has; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4–4 4 Reihungen und Abstraktionen PROCEDURE Sort* (VAR array : ARRAY OF Comparable); (*! Effect: Sort the whole array. !*) BEGIN (* To be implemented. *) ASSERT (Sorted (array), BEC.postcondition); END Sort; END I3ToolsetForArrayOfInteger. Schachteln oder nicht? Annehmend, dass die Funktion SortedBetween zur partiellen Sortiertprüfung für Kunden interessant sein kann, schachteln wir sie nicht in die Schnittstellenfunktion Sorted zur vollständigen Sortiertprüfung, sondern veröffentlichen sie. Umgekehrt nehmen wir bei der partiellen Suchfunktion HasBetween an, dass sie für Kunden uninteressant ist. Deshalb lassen wir sie geschützt, ja schachteln sie in die Schnittstellenfunktion Has zum vollständigen Suchen. So braucht HasBetween nur die Suchbereichsgrenzen left und right als Parameter, da es die Reihung array und das gesuchte Element item in seinem Kontext als Parameter von Has findet. Iterativ oder rekursiv? Sowohl SortedBetween als auch HasBetween sind in Programm 4.1 rekursiv implementiert. Auf den ersten Blick scheint SortedBetween nicht endrekursiv zu sein. Ersetzen wir den Ergebnisausdruck Partielle Sortiertprüfung Endrekursiver Aufruf (left >= right) OR ((array [left] <= array [left + 1]) & SortedBetween (array, left + 1, right)) mit den kurz ausgewerteten booleschen Operationen durch den äquivalenten mehrfach bedingten Ausdruck IF left >= right THEN TRUE ELSIF array [left] <= array [left + 1] THEN SortedBetween (array, left + 1, right) ELSE FALSE END so erkennen wir die Endrekursion. Die Zweige durch RETURN-Anweisungen ergänzend kehren wir in die imperative Welt bedingter Anweisungen zurück: Endrekursiver Aufruf IF left >= right THEN RETURN TRUE ELSIF array [left] <= array [left + 1] THEN RETURN SortedBetween (array, left + 1, right) ELSE RETURN FALSE END Auf diesen Algorithmus lässt sich das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 3-5 anwenden. Das Umformungsergebnis vereinfachen und verschönern wir zur folgenden iterativen Variante der partiellen Sortiertprüfung. Programm 4.2 Component Pascal: Partielle Sortiertprüfung, iterativ PROCEDURE SortedBetween* (IN array : ARRAY OF Comparable; left, right : Index) : BOOLEAN; (*! Postcondition: result = the elements of array with indices from left to right are ordered. !*) BEGIN ASSERT ((0 <= left) & (right < LEN (array)), BEC.precondParsConsistent); WHILE (left < right) & (array [left] <= array [left + 1]) DO INC (left); END; RETURN left >= right; END SortedBetween; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung Partielles Suchen 4–5 Im Rumpf der Suchfunktion HasBetween in Programm 4.1 erscheint der Erfolgsfall item = array [mid] als letzter Fall der mehrfachen Auswahlanweisung, da er bei großem Suchraum statistisch seltener als der Misserfolgsfall vorkommt und eine mehrfache Auswahl ihre Bedingungen möglichst nach fallender Häufigkeit des Erfülltseins geordnet prüfen soll. Der Algorithmus von HasBetween in Programm 4.1 mag als strukturiert gelten, da die Rücksprünge nur in Zweigen stehen, auf die keine Anweisung folgt. Wegen der Rücksprünge lassen sich Zweige einsparen: Partielles binäres Suchen, rekursiv, unstrukturiert ☺ IF left > right THEN RETURN FALSE; END; mid := (left + right) DIV 2; IF item < array [mid] THEN RETURN HasBetween (left, mid - 1); ELSIF item > array [mid] THEN RETURN HasBetween (mid + 1, right); END; RETURN TRUE; Verglichen mit Programm 3.9 S. 3-4, das einen Zweig nutzlos spart, verbuchen wir hier als Nutzen die um eins verringerte Schachtelungstiefe. Andererseits verliert die Variante an Struktur, weil der erste Rücksprung drei Anweisungen vor dem statischen Ende steht. Bei diesen konfligierenden Argumenten zur Verständlichkeit fällt es nicht leicht, zwischen beiden Varianten des Algorithmus zu entscheiden. Offenbar sind die rekursiven Varianten der partiellen Suchfunktion endrekursiv, sodass sich das Schema zur Umformung endrekursiver in iterative Algorithmen von S. 3-5 anwenden lässt. Da die damit erhaltene Bedingungsschleife mindestens einmal durchlaufen wird, wandeln wir sie von der kopfgesteuerten Form in die fußgesteuerte Form und setzen diese direkt in die vollständige Suchfunktion Has ein. Programm 4.3 Component Pascal: Binäres Suchen, iterativ, unstrukturiert PROCEDURE Has* (IN array : ARRAY OF Comparable; item : Comparable) : BOOLEAN; (*! Postcondition: result = array contains item. !*) VAR left, right, mid : Index; BEGIN ASSERT (Sorted (array), BEC.precondition); left := 0; right := LEN (array) - 1; REPEAT mid := (left + right) DIV 2; IF item < array [mid] THEN right := mid - 1; ELSIF item > array [mid] THEN left := mid + 1; ELSE RETURN TRUE; END; UNTIL left > right; RETURN FALSE; END Has; Wegen des Rücksprungs aus der Bedingungsschleife heraus ist diese Variante nicht strukturiert. Wer strukturierte Programme bevorzugt, erhält daraus mit einigen Umformungsschritten die folgende Variante mit kopfgesteuerter Schleife: 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4–6 Binäres Suchen, iterativ, strukturiert 4 Reihungen und Abstraktionen left := 0; right := LEN (array) - 1; mid := (left + right) DIV 2; WHILE (left <= right) & (item # array [mid]) DO IF item < array [mid] THEN right := mid - 1; ELSE left := mid + 1; END; mid := (left + right) DIV 2; END; RETURN left <= right; ☺ Welche Variante ist besser? Die letzte Variante ist zwar strukturiert, benötigt aber im Mittel drei Vergleiche pro Iteration, einen weiteren Vergleich nach der Schleife und berechnet mid einmal umsonst, wobei die Berechnung zweimal hingeschrieben ist. Die Variante in Programm 4.3 ist dagegen unstrukturiert, benötigt aber im Mittel nur zweieinhalb Vergleiche pro Iteration, keinen Vergleich nach der Schleife und berechnet nichts umsonst. Hier konfligieren also Effizienz und Verständlichkeit. 4.1.4 Eiffel 4.1.4.1 Werkzeugkasten Verallgemeinern In Eiffel, das kein Modulkonstrukt kennt, sehen wir eine Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE vor. Mit Generizität bietet Eiffel eine Möglichkeit, Lösungen zu verallgemeinern. Generische Klassen lassen sich durch die Variation statischer Typen wiederverwenden, ohne an statischer Typsicherheit zu verlieren. Wir nutzen das Konzept der generischen Klassen, um vom Elementtyp INTEGER zu abstrahieren. Das Problem ist schon mit einem generischen Elementtyp E lösbar, für den eine Vollordnung definiert ist. Daher genügt es zu fordern, dass E die Vollordnung von der Standardbibliotheksklasse COMPARABLE erbt: E -> COMPARABLE COMPARABLE COMPARABLE definiert die Relationen =, /=, <, <=, >, >= in üblicher Infixnotation teilweise abstrakt, teilweise implementiert. Die Definition der generischen Werkzeugkastenklasse beginnt mit class TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE] Bei der Vereinbarung einer Größe dieser Klasse ist der formale generische Parameter E zu konkretisieren, etwa so: tsac : TOOLSET_ARRAY_COMPARABLE [INTEGER] Für E als aktuellen generischen Parameter INTEGER eingesetzt liefert die verlangte spezielle Lösung für ganzzahlige Reihungen. Eingeschränkte und uneingeschränkte Generizität Da der generische Parameter die Vererbungsbeziehung erfüllen muss, spricht man von eingeschränkter Generizität (restricted genericity). Dagegen bedeutet uneingeschränkte Generizität (unrestricted genericity), dass an generische Parameter keine expliziten Bedingungen gestellt werden können. Eingeschränkte Generizität ist ausdrucksfähiger als uneingeschränkte Generizität und erlaubt die Prüfung explizit formulierter Einschränkungen an der Stelle der Definition einer generischen Einheit. Uneingeschränkte Generizität ist flexibler einsetzbar als eingeschränkte Generizität, aber die implizit dennoch vorhandenen Einschränkungen lassen sich erst bei der Benutzung einer generischen Einheit prüfen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung Programm 4.4 Eiffel: Sortiertprüfung iterativ; binäres Suchen, rekursiv 4–7 note description: "Tools operating on arrays of comparable elements" class TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE] feature sorted_between (array : ARRAY [E]; left, right : INTEGER) : BOOLEAN -- Are the elements of array with indices from left to right ordered? require array_exists: array /= Void left_large_enough: array.lower <= left right_small_enough: right <= array.upper local index : INTEGER do from index := left until index >= right or else array.item (index) > array.item (index + 1) loop index := index + 1 end Result := index >= right end -- sorted_between sorted (array : ARRAY [E]) : BOOLEAN -- Are the elements of array ordered? require array_exists: array /= Void do Result := sorted_between (array, array.lower, array.upper) end -- sorted feature {NONE} has_between (array : ARRAY [E]; item : E; left, right : INTEGER) : BOOLEAN -- Does array contain item between positions left and right? require array_exists: array /= Void left_large_enough: array.lower <= left right_small_enough: right <= array.upper sorted: sorted_between (array, left, right) local mid : INTEGER do if left <= right then mid := (left + right) // 2 if item < array.item (mid) then Result := has_between (array, item, left, mid - 1) elseif item > array.item (mid) then Result := has_between (array, item, mid + 1, right) else Result := True end end end -- has_between 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4–8 4 Reihungen und Abstraktionen feature has (array : ARRAY [E]; item : E) : BOOLEAN -- Does array contain item? require array_exists: array /= Void sorted: sorted (array) do Result := has_between (array, item, array.lower, array.upper) end -- has sort (array : ARRAY [E]) -- Sort the elements of array in ascending order. require array_exists: array /= Void do -- To be implemented. ensure sorted: sorted (array) end -- sort end -- class TOOLSET_ARRAY_COMPARABLE Formal und aktuell In der Signatur sorted_between (array : ARRAY [E]; left, right : INTEGER) : BOOLEAN ist der formale Parameter array eine Referenz auf ein Objekt des Typs ARRAY [E]. Die Parameterübergabeart entspricht der Eingabe-Referenzübergabe (IN-Parameter in Component Pascal). Als aktueller generischer Parameter der generischen Klasse ARRAY wird hier der formale generische Parameter E der generischen Klasse TOOLSET_ARRAY_COMPARABLE eingesetzt. Implizites Initialisieren Eiffel initialisiert alle Attribute und lokalen Variablen implizit mit Standardwerten: Zahlen mit Nullen, boolesche Größen mit False. Deshalb kann has_between einen Zweig sparen. Strukturiertes Programmieren Während Algorithmen in Component Pascal und C* nicht den strengen Anforderungen strukturierten Programmierens entsprechen müssen, da sie wegen möglicher Rücksprünge aus Alternativen und Schleifen mehrere Ausgänge haben können, erfüllen alle Eiffel-Algorithmen die 1-Ein-/1-Ausgang-Regel. In Eiffel ist es unmöglich, unstrukturiert zu programmieren! Aufgabe 4.1 Eiffel: Sortiertprüfung rekursiv, binäres Suchen iterativ Implementieren Sie eine (1) (2) rekursive Eiffel-Variante der Sortiertprüfungsfunktion sorted_between, iterative Eiffel-Variante der Suchfunktion has als Teil der Klasse TOOLSET_ARRAY_COMPARABLE! Zeigen Sie jeweils, dass sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten überführen lässt! 4.1.4.2 Testtreiber Um das binäre Suchen der Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE testen zu können, schreiben wir zunächst eine einfache Testtreiberklasse TESTER_OF_TSACI_HAS. Später werden wir diesen Testtreiber verallgemeinern und wiederverwendbare Teile herausfaktorisieren. Methodisch gesehen realisiert der Testtreiber einen randomisierten Einzeltest. Damit lassen sich schnell viele verschiedene Testfälle abdecken. Ein Testfall besteht aus einer ganzzahligen Reihung beliebiger Länge und einer gesuchten Ganzzahl. Statt Testfälle manuell einzugeben, lassen wir sie im Testtreiber von einem Zufallszahlengenerator erzeugen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung Zufallszahlengenerator Einmalfunktion 4–9 RANDOM ist eine Bibliotheksklasse für Zufallszahlen. Sie funktioniert nach dem in Eif- fel üblichen Kommando-Abfrage-Schema, da in Eiffel Nebeneffekte verpönt sind. Entsprechend definieren wir random_item als nebeneffektfreie Funktion. Das benötigte RANDOM-Objekt wird von der Einmalfunktion (siehe S. 3-22) random nur beim ersten Aufruf erzeugt, alle folgenden Aufrufe von random liefern eine Referenz auf dasselbe Objekt. Geschützte Wurzelprozedur Erzeugungsprozeduren können auch durch feature {NONE} geschützt sein. Sie sind dann nur aus Nachfolgerklassen und als Wurzelprozeduren aufrufbar. Ausgabe Die Reihung wird mit der von ANY geerbten print-Prozedur ausgegeben. Wirkt sie nicht wie gewünscht, so sollte man statt print das von ANY geerbte Merkmal out : STRING redefinieren, welches das Objekt druckbar darstellt. Programm 4.5 Eiffel: Testtreiber zu binärem Suchen note description: "Test driver for type TOOLSET_ARRAY_COMPARABLE [INTEGER]" class TESTER_OF_TSACI_HAS create make feature {NONE} Min : INTEGER = -1000 Max : INTEGER = 1000 random : RANDOM once create Result.make ensure random_exists: Result /= Void end -- random random_item : INTEGER require random_exists: random /= Void do Result := (((random.item - 1) / (random.modulus - 2)) * (Max - Min) + Min).floor ensure large_enough: Min <= Result small_enough: Result <= Max end -- random_item init_random (array : ARRAY [INTEGER]) require random_exists: random /= Void array_exists: array /= Void local index : INTEGER do from index := array.lower until index > array.upper loop random.forth array.put (random_item, index) index := index + 1 end end -- init_random 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 10 4 Reihungen und Abstraktionen make -- Given a positive integer command argument, fill an integer array of given -- size with random values and check if it contains another random value. local command_line : ARGUMENTS tsaci : TOOLSET_ARRAY_COMPARABLE [INTEGER] array : ARRAY [INTEGER] item : INTEGER do create command_line if command_line.argument_count < 1 or else not command_line.argument (1).is_integer or else command_line.argument (1).to_integer <= 0 then io.put_string ("Please call this command with a positive integer argument" + " for the array size!") else create tsaci create array.make (1, command_line.argument (1).to_integer) init_random (array) tsaci.sort (array) random.forth item := random_item io.put_string ("array: "); print (array) io.put_string ("item "); io.put_integer (item) if not tsaci.has (array, item) then io.put_string (" not") end io.put_string (" found in array.") end io.put_new_line end -- make end -- class TESTER_OF_TSACI_HAS 4.1.5 C++ C++ kennt zwar kein Modulkonstrukt, bietet aber zwei Möglichkeiten, ein Werkzeugkastenmodul zu realisieren, die wir beide vorstellen: Namensräume und Klassenfunktionen. Auch für die generische Verallgemeinerung gibt es zwei Möglichkeiten, die wir beide vorstellen: generische Funktionen und generische Klassen, der hybriden prozedural-objektorientierten Konzeption von C++ entsprechend. Im Unterschied zu Eiffel kennt C++ nur uneingeschränkte Generizität; die Bezeichnung dafür ist Schablone (template). 4.1.5.1 Werkzeugkasten: Namensraum und generische Funktionen Die erste C++-Variante verbindet das prozedural-modulare Muster der ComponentPascal-Variante mit der generischen Verallgemeinerung der Eiffel-Variante. Die Module von Component Pascal lassen sich in C++ mit Namensräumen nachbilden. Modulnamen in Component Pascal setzen sich aus zwei Teilen zusammen, z.B. I3ToolsetForArrayOfInteger aus dem Subsystemnamen I3 und dem Moduldateinamen ToolsetForArrayOfInteger. Nur der Subsystemname muss systemweit eindeutig sein. Der Zugriff erfolgt mit der Punktnotation: b := I3ToolsetForArrayOfInteger.Sorted (array); © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung 4 – 11 Die zweistufige Namensgebung lässt sich in C++ durch zwei geschachtelte Namensräume realisieren: Geschachtelte Namensräume namespace I3 { namespace ToolsetArrayInteger { bool sorted (int array []); } // end of ToolsetArrayInteger } // end of I3 Entsprechend wird zweistufig qualifiziert mit dem Gültigkeitsbereichsauflösungsoperator zugegriffen: b = I3::ToolsetArrayInteger::sorted(array); Eiffel bietet keine mehrstufigen Namensräume. Bei der Eiffel-Variante muss der Klassenname TOOLSET_ARRAY_COMPARABLE systemweit eindeutig sein. Deshalb begnügen wir uns der Einfachheit halber auch bei der C++-Variante mit einstufiger Namensgebung: Einfacher Namensraum namespace ToolsetArrayInteger { bool sorted (int array []); } // end of ToolsetArrayInteger Es wird einstufig qualifiziert zugegriffen: b = ToolsetArrayInteger::sorted(array); Kapseln und schützen Component-Pascal-Module und C++-Namensräume unterscheiden sich darin, dass Module auch Schutzeinheiten sind, die ihre Merkmale vor externen Zugriffen schützen können, während Namensräume Merkmale nur kapseln, nicht schützen. Speichern und übersetzen Während in Component Pascal Module, Quelltextdateien und Übersetzungseinheiten sich eineindeutig entsprechen, stehen in C++ Namensräume, Quelltextdateien und Übersetzungseinheiten orthogonal zueinander. Beispielsweise kann sich ein Namensraum über mehrere Quelltextdateien erstrecken, eine Quelltextdatei kann Teile mehrerer Namensräume enthalten. Der Programmierer kann die Beziehungen zwischen diesen Einheiten flexibel gestalten, das Ergebnis kann aber unübersichtlich werden. Deshalb legen wir mit einer an Component Pascal orientierten Konvention fest, wie wir Module in den C++-Beispielprogrammen dieses Skripts realisieren: Realisierung von Modulen Zu einem Modul gehören höchstens eine Schnittstellendatei und höchstens eine Implementationsdatei, mindestens aber eine von beiden. Alle Teile eines Moduls gehören zu genau einem Namensraum, der nur dieses Modul umfasst. Jede Quelltextdatei enthält höchstens ein Modul und einen zugehörigen Namensraum. Die von C geerbten Reihungen, vereinbart z.B. durch C-Reihung int array []; sind unsicher, da zur Laufzeit nicht prüfbar ist, ob ein Index innerhalb der zulässigen Grenzen liegt. Deshalb hält sich der professionelle Programmierer an die von Stroustrup empfohlene Programmierleitlinie [Str97], S. 14: Leitlinie 4.1 C++: Benutze Vektoren statt Reihungen 27.9.12 Vermeide, die unsicheren C-Reihungen zu verwenden! Verwende stattdessen die sichere STL-Klasse vector! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 12 4 Reihungen und Abstraktionen STL ist die Abkürzung für die Standard Template Library, die zum ANSI/ISO-Standard von C++ gehört. Mit der generischen Klasse vector, zu deren Benutzung ihre Schnittstellendatei durch #include <vector> zu inkludieren ist, ist C++-Vektor std::vector<int> array; eine entsprechende Vereinbarung. Programm 4.6 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv; Schnittstelle mit generischen Funktionen // Header file: ToolsetArrayComparableM.hpp #ifndef TOOLSET_ARRAY_COMPARABLE_M_HPP_ #define TOOLSET_ARRAY_COMPARABLE_M_HPP_ #include <cassert> #include <vector> namespace ToolsetArrayComparable { template <typename T> bool sorted_between (const std::vector<T> & array, int left, int right) // Instantiation condition: T is a comparable type. { assert(left >= 0); // precondition assert(right < array.size()); // precondition while (left < right && array[left] <= array[left + 1]) { ++left; } return left >= right; } template <typename T> bool sorted (const std::vector<T> & array) // Instantiation condition: T is a comparable type. { return sorted_between(array, 0, array.size() - 1); } template <typename T> bool has_between (const std::vector<T> & array, const T & item, int left, int right) // Instantiation condition: T is a comparable type. { assert(left >= 0); // precondition assert(right < array.size()); // precondition assert(sorted_between(array, left, right)); // precondition if (left > right) { return false; } else { int mid = (left + right) / 2; if (item < array[mid]) { return has_between(array, item, left, mid - 1); } else if (item > array[mid]) { return has_between(array, item, mid + 1, right); } else { return true; } } } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung 4 – 13 template <typename T> bool has (const std::vector<T> & array, const T & item) // Instantiation condition: T is a comparable type. { assert(sorted(array)); // precondition return has_between(array, item, 0, array.size() - 1); } template <typename T> void sort (std::vector<T> & array) // Instantiation condition: T is a comparable type. { // To be implemented. assert(sorted(array)); // postcondition } } // ToolsetArrayComparable #endif // TOOLSET_ARRAY_COMPARABLE_M_HPP_ Generische Funktion template <typename T> bool sorted (const std::vector<T> & array) deklariert die generische Funktion sorted mit dem formalen generischen Parameter T. Statt template <typename T> könnten wir template <class T> schreiben, doch da wir T durch Basistypen wie int und float konkretisieren wollen, die keine Klassen sind, passt typename besser als class. In bool sorted (const std::vector<T> & array) ist der formale Parameter array eine Referenz (&) auf ein konstantes Objekt (const) vom Typ std::vector<T>. Die Parameterübergabeart entspricht der Eingabe-Referenzübergabe (IN-Parameter in Component Pascal). Die generische Klasse vector gehört zum Namensraum std; als aktueller generischer Parameter für vector wird hier der formale generische Parameter T von sorted eingesetzt. Einschränkungen an T sind nicht ausdrückbar. Erst die Implementation von sorted offenbart, dass für T die Vergleichsoperatoren definiert sein müssen. Der Kommentar // Instantiation condition: T is a comparable type. soll Anwender vorab darauf aufmerksam machen. Implementation in der Schnittstelle Normalerweise enthält eine Schnittstellendatei nur die Deklarationen der exportierten Funktionen. Doch bei generischen Funktionen (Klassen) muss die Schnittstellendatei auch die Implementationen der Funktionen (Klassen) vollständig enthalten, denn nur so kann der Kompilierer an inkludierenden Stellen Code für Konkretisierungen der Funktionen (Klassen) erzeugen – eine Folge der uneingeschränkten Generizität. Eine Konsequenz davon ist, dass die in der Eiffel-Variante versteckt gehaltene generische Funktion has_between in dieser C++-Variante mit ihrer Implementation in die Schnittstellendatei aufgenommen werden muss und damit öffentlich ist. Indexbereich Der Indexbereich von vector-Objekten beginnt wie der von C-Reihungen bei 0. Im Unterschied zu C-Reihungen ist bei vector-Objekten die Anzahl der Elemente dynamisch mit der size()-Funktion abfragbar. Mehrseitige Auswahl Die C*-Sprachen kennen nur die zweiseitige if-else-Auswahlanweisung. Mehrseitige Auswahlen sind durch geschachtelte if-else zu realisieren. Statt bei jedem geschachtelten if-else tiefer einzurücken: 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 14 4 Reihungen und Abstraktionen if (...) { ... } else { if (...) { ... } else { if (...) { ... } else { ... } } } empfiehlt sich, die Konvention des Blockens der Zweige zu lockern und nach else geschachtelte if-else-Anweisungen nicht zu klammern: ☺ Aufgabe 4.2 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ if (...) { ... } else if (...) { ... } else if (...) { ... } else { ... } Implementieren Sie eine (1) rekursive C++-Variante der generischen Sortiertprüfungsfunktion sorted_between, (2) iterative C++-Variante der generischen Suchfunktion has als Teil der Schnittstellendatei ToolsetArrayComparableM.hpp! Zeigen Sie jeweils, dass sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten überführen lässt! 4.1.5.2 Werkzeugkasten: Generische Klasse und Klassenfunktionen Eine alternative Realisierung des Werkzeugkastenmoduls verwendet anstelle der generischen Funktionen ähnlich wie die Eiffel-Variante eine generische Klasse. Die Deklaration der generischen Werkzeugkastenklasse beginnt mit Generische Klasse template <typename T> class ToolsetArrayComparable Da diese Klasse nur Werkzeuge bietet und keinen Zustand hat, genügt von ihr ein Objekt. Die Situation entspricht dem Entwurfsmuster Singleton. Während Eiffel Singletons und Module nicht sehr gut unterstützt, bietet C++ dafür Klassenfunktionen, also Funktionen, die durch den Spezifikator static an die Klasse gebunden und sich mit dem Klassennamen qualifiziert aufrufen lassen. Bei generischen Klassen sind die formalen generischen Parameter zu konkretisieren, hier etwa so: ToolsetArrayComparable<int>::sorted(array) Für T als aktuellen generischen Parameter int eingesetzt liefert die verlangte spezielle Lösung für ganzzahlige Reihungen. Da die Klasse einen eigenen Namensraum definiert, verzichten wir bei dieser Variante auf einen zusätzlichen Namensraum. template <typename T> class ToolsetArrayComparable { ... } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung Programm 4.7 C++: Sortiertprüfung, iterativ; binäres Suchen, rekursiv; Schnittstelle mit generischer Klasse 4 – 15 // Header file: ToolsetArrayComparableC.hpp #ifndef TOOLSET_ARRAY_COMPARABLE_C_HPP_ #define TOOLSET_ARRAY_COMPARABLE_C_HPP_ #include <cassert> #include <vector> template <typename T> class ToolsetArrayComparable // Instantiation condition: T is a comparable type. { public: static bool sorted_between (const std::vector<T> & array, int left, int right); static bool sorted (const std::vector<T> & array); static bool has (const std::vector<T> & array, const T & item); static void sort (std::vector<T> & array); protected: static bool has_between (const std::vector<T> & array, const T & item, int left, int right); }; // ToolsetArrayComparable template <typename T> bool ToolsetArrayComparable<T>::sorted_between (const std::vector<T> & array, int left, int right) { assert(left >= 0); // precondition assert(right < array.size()); // precondition while (left < right && array[left] <= array[left + 1]) { ++left; } return left >= right; } template <typename T> bool ToolsetArrayComparable<T>::sorted (const std::vector<T> & array) { return sorted_between(0, array.size() - 1); } template <typename T> bool ToolsetArrayComparable<T>::has_between (const std::vector<T> & array, const T item, int left, int right) { assert(left >= 0); // precondition assert(right < array.size()); // precondition assert(sorted_between(array, left, right)); // precondition if (left > right) { return false; } else { int mid = (left + right) / 2; if (item < array[mid]) { return has_between(array, item, left, mid - 1); } else if (item > array[mid]) { return has_between(array, item, mid + 1, right); } else { return true; } } } 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 16 4 Reihungen und Abstraktionen template <typename T> bool ToolsetArrayComparable<T>::has (const std::vector<T> & array, const T item) { assert(sorted(array)); // precondition return has_between(array, item, 0, array.size() - 1); } template <typename T> void ToolsetArrayComparable<T>::sort (std::vector<T> & array) { // To be implemented. assert(sorted(array)); // postcondition } #endif // TOOLSET_ARRAY_COMPARABLE_C_HPP_ deklariert die generische Klasse ToolsetArrayComparable mit dem formalen generischen Parameter T. Zwischen den Klassenklammern steht nur die Schnittstelle der Klasse. Implementationen von Klassen- und Elementfunktionen stehen außerhalb davon, normalerweise in der zugehörigen Implementationsdatei. Da es sich hier jedoch um eine generische Klasse handelt, müssen aus dem oben genannten Grund alle Implementationen in der Schnittstellendatei enthalten sein. Aufgabe 4.3 C++: Sortiertprüfung rekursiv, binäres Suchen iterativ Implementieren Sie eine (1) (2) rekursive C++-Variante der Sortiertprüfungs-Klassenfunktion sorted_between, iterative C++-Variante der Such-Klassenfunktion has als Teil der Schnittstellendatei ToolsetArrayComparableC.hpp! Zeigen Sie jeweils, dass sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten überführen lässt! 4.1.5.3 Testtreiber Um die Schnittstellendateien ToolsetArrayComparableM.hpp und ToolsetArrayComparableC.hpp testen zu können, schreiben wir analog zur Eiffel-Variante zunächst einfache Testtreibermodule TesterOfTSACIMHas und TesterOfTSACICHas, die später zu verallgemeinern sind. Programm 4.8 C++: Testtreiber zu binärem Suchen mit generischen Funktionen // Implementation file: TesterOfTSACIMHas.cpp #include <cstdlib> #include <iostream> #include "ToolsetArrayComparableM.hpp" typedef long Element; static const Element min = -1000; static const Element max = 1000; static Element random_item () { Element result = Element((double(std::rand() - 1) / (RAND_MAX - 2)) * (max - min) + min); assert(min <= result); // postcondition assert(result <= max); // postcondition return result; } static void init_random (std::vector<Element> & array) { for (int index = 0; index < array.size(); ++index) { array[index] = random_item(); } } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung 4 – 17 static std::ostream& operator<< (std::ostream& s, const std::vector<Element> & array) { s << "["; for (int index = 0; index < array.size(); ++index) { s << array[index] << " "; } s << "]" << std::endl; } int main (int argc, char * argv[]) { if (argc < 2 || std::atoi(argv[1]) <= 0) { std::cout << "Please call this command with a positive integer argument" + " for the array size!"; } else { std::vector<Element> array (std::atoi(argv[1])); init_random(array); ToolsetArrayComparable::sort(array); Element item = random_item(); std::cout << "array: " << array << "item " << item << ((ToolsetArrayComparable::has(array, item)) ? "" : " not") << " found in array."; } std::cout << std::endl; return 0; } Zufallszahlengenerator rand ist eine nebeneffektbehaftete Bibliotheksfunktion für Zufallszahlen. Entsprechend definieren wir random_item als nebeneffektbehaftete Funktion. Ausgabe Um die Reihung auszugeben, ist eine Prozedur zu definieren. Das C++-typische Idiom dafür ist, den <<-Operator zu überladen. Der Testtreiber für die Klassenvariante unterscheidet sich vom Testtreiber für die Modulvariante nur an wenigen Stellen. Programm 4.9 C++: Testtreiber zu binärem Suchen mit generischer Klasse // Implementation file: TesterOfTSACICHas.cpp ... #include "ToolsetArrayComparableC.hpp" ... ToolsetArrayComparable<Element>::sort(array); ... << ((ToolsetArrayComparable<Element>::has(array, item)) ? "" : " not") ... 4.1.6 Java 4.1.6.1 Werkzeugkasten Ursprünglich ohne Generizität entworfen, unterstützt Java seit Release 5.0 das von Eiffel und C++ bekannte Konzept in Form generischer Klassen und Schnittstellen; die Bezeichnung dafür ist auch parametrisierter Typ (parameterized type). Wie Eiffel erlaubt Java eingeschränkte Generizität; generische Typen sind durch eine Klasse und beliebig viele Schnittstellen einschränkbar. Entsprechend stellen wir eine generische Werkzeugkastenklasse ToolsetArrayComparable vor. Für den generischen Elementtyp E fordern wir, dass er die generische Schnittstelle Comparable<T> aus dem Java Collections Framework implementiert: E extends Comparable<E> 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 18 4 Reihungen und Abstraktionen (Java verwendet das Schlüsselwort implements für das Implementieren von Schnittstellen, trotzdem muss hier extends statt implements stehen.) E erscheint sowohl als formaler als auch als aktueller generischer Parameter, damit zwei Elemente gleichen Typs vergleichbar werden. Dagegen braucht die Eiffel-Klasse COMPARABLE nicht generisch zu sein, da Eiffel das Problem anders – mit verankerten Typen – löst. Comparable Die Schnittstelle Comparable<T> hat nur eine einzige Methode mit der Signatur int compareTo (T o) und der Semantik, einen negativen oder positiven Wert oder 0 zu liefern, wenn das this-Objekt (links) kleiner, größer bzw. gleich dem o-Objekt (rechts) ist. Dem Vorteil der schlanken Schnittstelle steht der Nachteil unhandlicher Vergleiche entgegen. Die Definition der generischen Werkzeugkastenklasse beginnt mit public class ToolsetArrayComparable<E extends Comparable<E>> fungiert nicht wie bei der C++-Variante Programm 4.7 als Modul, da Klassenfunktionen anscheinend nicht mit generischen Typen arbeiten können. Bei der Vereinbarung eines Referenzobjekts dieser Klasse ist der formale generische Parameter E zu konkretisieren, etwa so: ToolsetArrayComparable ToolsetArrayComparable<Integer> tsaci = new ToolsetArrayComparable<Integer>(); Für E als aktuellen generischen Parameter die Hüllenklasse Integer eingesetzt liefert die verlangte spezielle Lösung für ganzzahlige Reihungen. Programm 4.10 Java: Sortiertprüfung, iterativ; binäres Suchen, rekursiv public class ToolsetArrayComparable<E extends Comparable<E>> { public boolean sortedBetween (E[] array, int left, int right) { assert array != null : "precondition"; assert left >= 0 : "precondition"; assert right < array.length : "precondition"; while (left < right && array[left].compareTo(array[left + 1]) <= 0) { ++left; } return left >= right; } public boolean sorted (E[] array) { assert array != null : "precondition"; return sortedBetween(array, 0, array.length - 1); } private boolean hasBetween (E[] array, E item, int left, int right) { assert array != null : "precondition"; assert left >= 0 : "precondition"; assert right < array.length : "precondition"; assert sortedBetween(array, left, right) : "precondition"; if (left > right) { return false; } else { int mid = (left + right) / 2; if (item.compareTo(array[mid]) < 0) { return hasBetween(array, item, left, mid - 1); } else if (item.compareTo(array[mid]) > 0) { return hasBetween(array, item, mid + 1, right); } else { return true; } } } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung 4 – 19 public boolean has (E[] array, E item) { assert array != null : "precondition"; assert sorted(array) : "precondition"; return hasBetween(array, item, 0, array.length - 1); } public void sort (E[] array) { assert array != null : "precondition"; // To be implemented. assert sorted(array) : "postcondition"; } } Aufgabe 4.4 Java: Sortiertprüfung rekursiv, binäres Suchen iterativ Implementieren Sie eine (1) (2) rekursive Java-Variante der Sortiertprüfungsfunktion sortedBetween, iterative Java-Variante der Suchfunktion has als Teil der Klasse ToolsetArrayInteger! Zeigen Sie jeweils, dass sich die rekursive Variante nach dem Umformungsschema von S. 3-5 in die iterative Varianten überführen lässt! 4.1.6.2 Testtreiber Zum Testen der Werkzeugkastenklasse ToolsetArrayInteger schreiben wir analog zu den Eiffel- und C++-Varianten eine einfache Testtreiberklasse TesterOfTSACIHas. Zufallszahlengenerator Random ist eine Klasse für Zufallszahlen mit einer nebeneffektbehafteten Funktion nextInt. Entsprechend definieren wir randomItem als nebeneffektbehaftete Funktion. Ausgabe Um die Reihung auszugeben, ist eine normale Prozedur zu definieren. Programm 4.11 Java: Testtreiber zu binärem Suchen public class TesterOfTSACIHas { private static final int MIN = -1000; private static final int MAX = 1000; private static java.util.Random random = new java.util.Random(); private static int randomItem () { assert random != null : "precondition"; int result = random.nextInt(MAX - MIN + 1) - MIN; assert MIN <= result : "postcondition"; assert result <= MAX : "postcondition"; return result; } private static void initRandom (Integer[] array) { assert random != null : "precondition"; assert array != null : "precondition"; for (int index = 0; index < array.length; ++index) { array[index] = randomItem(); } } private static void print (Integer[] array) { assert array != null : "precondition"; System.out.print("["); for (int index = 0; index < array.length; ++index) { System.out.print(array[index] + " "); } System.out.println("]"); } private static final String HINT = "Please call this command with a positive integer argument" + " for the array size!"; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 20 4 Reihungen und Abstraktionen public static void main (String[] args) { if (args.length < 1) { System.out.println (HINT); return; } int size = 0; try { size = Integer.parseInt(args[0]); } catch (Exception e) { System.out.println (HINT); return; } if (size <= 0) { System.out.println (HINT); return; } ToolsetArrayComparable<Integer> tsaci = new ToolsetArrayComparable<Integer>(); Integer[] array = new Integer[size]; initRandom(array); tsaci.sort(array); int item = randomItem(); System.out.print("array: "); print(array); System.out.print("item " + item); if (!ToolsetArrayInteger.has(array, item)) { System.out.print(" not"); } System.out.print(" found in array."); } } Erlaubte Typarten Java erlaubt als Typarten für aktuelle generische Parameter nur Klassen, keine primitiven Typen, da diese keine Klassen sind. Statt primitive Typen muss man ihre Hüllenklassen nehmen (int → Integer). Automatisches Ver-/Enthüllen (autoboxing) soll die Handhabung vereinfachen. Eiffel und C++ erlauben alle Typarten für aktuelle generische Parameter; Eiffel wegen seines homogenen Typsystems, bei dem alle Klassen von der allgemeinen Klasse ANY erben; C++ wegen der Makroexpansion als Übersetzungstechnik. Java bleibt einerseits mit seinem heterogenen Typsystem näher bei C++ und kann nicht wie Eiffel die Vorteile eines homogenen Typsystems nutzen. Andererseits verzichtet Java auf die primitive, aber flexible Technik der Makroexpansion von C++. 4.1.7 C# Wie Java ursprünglich ohne Generizität entworfen, unterstützt C# ab dem .NET 2.0 Framework generische Klassen und Schnittstellen. Wie Eiffel und Java erlaubt C# eingeschränkte Generizität; generische Typen sind durch eine Klasse und beliebig viele Schnittstellen einschränkbar. Entsprechend ist auch in C# eine generische Werkzeugkastenklasse ToolsetArrayComparable zu realisieren. Für den generischen Elementtyp E fordern wir, dass er die generische Schnittstelle IComparable<T> aus der .NET Framework Class Library implementiert: public class ToolsetArrayComparable<E> where E : IComparable<E> Aufgabe 4.5 C#: Sortiertprüfung, binäres Suchen Entwickeln Sie rekursive und iterative C#-Varianten zu den Problemen von 4.1! Entwickeln Sie auch einen Testtreiber dazu! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.1 Binäres Suchen in einer sortierten Reihung 4.1.8 4 – 21 Fazit Sortiertprüfung und binäres Suchen sind funktionale Probleme, die sich mit rekursiven und iterativen Algorithmen lösen lassen. Die Implementationen der Algorithmen in den Auswahlsprachen unterscheiden sich nur unwesentlich. Die Problemlösungen werden in einer Werkzeugkastenkomponente zusammengefasst. Bei den generischen Verallgemeinerungen der Werkzeugkastenkomponenten zeigen sich Gemeinsamkeiten und Unterschiede: In Eiffel, C++, Java und C# lässt sich die Werkzeugkastenkomponente als generische Klasse realisieren. Eiffel, Java und C# erlauben eingeschränkte Generizität; damit lässt sich die Einschränkung, dass für den Elementtyp eine Vollordnung definiert ist, explizit spezifizieren. In C++ ist diese Einschränkung implizit in der Implementation der generischen Klasse versteckt. Eiffel, C++ und C# erlauben beliebige Typen als aktuelle generische Parameter, sofern sie die Einschränkungen erfüllen. Java erlaubt nur Klassen als aktuelle generische Parameter, sodass statt primitiver Typen ihre Hüllenklassen einzusetzen sind. Unterstützt die Sprache auch generische Module? Eiffel ermöglicht generische Module nur wie allgemeine Module über Einmalfunktionen, während die C*-Sprachen Module durch Klassenfunktionen ermöglichen. C++ erlaubt generische Klassen mit Klassenfunktionen, die mit generischen Parametern arbeiten, sodass solche Klassen als generische Module fungieren können. Javas Klassenfunktionen können nicht mit generischen Parametern arbeiten, sodass sich generische Module nur ähnlich wie in Eiffel realisieren lassen. C# erlaubt generische Klassenfunktionen, sodass Klassen mit solchen generischen Klassenfunktionen als generische Module fungieren können. C++ kennt auch generische Funktionen und ermöglicht zusammen mit Namensräumen eine Realisierung, die einem generischen Modul nahe kommt. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 22 4.2 4 Reihungen und Abstraktionen Sortieren einer Reihung mit Quicksort Sortieren von Daten ist eine oft vorkommende Aufgabe in der Informatik. Problem Sortieren Gegeben: Eine Reihung array von Elementen mit einer Vollordnungsrelation ≤. Gesucht: Die Reihung mit denselben Elementen, aber aufsteigend geordnet. Mathematisch lässt sich das Problem als Abbildung auffassen, etwa sort : Gn → Gn. Die passende imperative Lösung ist eine Prozedur mit der Reihung als Ein-/AusgabeParameter, implementiert mit einem rekursiven oder iterativen Algorithmus. Wir ordnen Sortierprozeduren der Werkzeugkastenkomponente von 4.1 zu. Quicksort Quicksort ist ein von C. A. R. Hoare 1962 erfundener beliebter effizienter Sortieralgorithmus, der sich gut rekursiv formulieren lässt und einen Laufzeitaufwand der Größenordnung O(n * log(n)) hat. Da Quicksort in zahllosen Lehrbüchern beschrieben ist, genügt hier eine kurze Beschreibung. Quicksort nutzt das Teile-und-herrsche-Prinzip: Wähle ein Element der Reihung als Trennelement, bringe alle kleineren Elemente nach links und alle größeren Elemente nach rechts. Behandle die Reihungsteile links und rechts des Trennelements ebenso. Viele Algorithmen lassen sich mit Listenkonstruktoren kompakt formulieren. Als Beispiel geben wir eine funktionale rekursive Implementation von Quicksort in der funktionalen Programmiersprache Miranda an, die als Spezifikation dienen soll. Programm 4.12 Miranda: Quicksort quicksort [ ] = [ ] quicksort (h : t) = quicksort [ x | x <- t; x <= h ] ++ [ h ] ++ quicksort [ x | x <- t; x > h ] Hier bedeuten: [] h:t [ x | x <- t; x <= h ] ++ leere Liste, Liste mit Kopf h wie head und Rumpf t wie tail, Liste aller x aus t, die kleinergleich h sind, Konkatentation von Listen. Der kompakten Formulierung der Lösung stehen Effizienzverluste durch das dynamische Erzeugen der vielen Teillisten gegenüber. Wir ordnen auch die Quicksort-Prozedur der Werkzeugkastenkomponente von 4.1 zu. Als Testtreiber können wir die Testtreiber zum binären Suchen verwenden, da sie die Reihungen vor dem Suchen sortieren müssen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.2 Sortieren einer Reihung mit Quicksort 4.2.1 Programm 4.13 Component Pascal: Quicksort, rekursiv 4 – 23 Component Pascal MODULE I3ToolsetForArrayOfInteger; (* Other stuff here. *) PROCEDURE Quicksort* (VAR array : ARRAY OF Comparable); (*! Effect: Sort the whole array. Recursive variant. !*) PROCEDURE SortBetween (low, high : Index); (* Sort the array slice [low .. high]. Choose an arbitrary separation element contained in the slice and repeat swapping two elements being less and greater than the separation element. The result are two subslices: The left (right) subslice contains all elements that are less (greater) than or equal to the separation element. The subslices are sorted recursively this way. *) VAR left, right : Index; separator : Comparable; BEGIN ASSERT ((0 <= low) & (low <= high) & (high < LEN (array)), BEC.precondition); left := low; right := high; separator := array [MR.UniformI (low, high)]; (* The random choice is inefficient, but it shows that the position of the separator is meaningless. Accelerate e.g. by using the middle index (low + high) DIV 2. *) REPEAT WHILE array [left] < separator DO INC (left); END; WHILE separator < array [right] DO DEC (right); END; (* ASSERT ((array [left] >= separator) & (separator >= array [right])); *) IF left <= right THEN Swap (array, left, right); INC (left); DEC (right); END; UNTIL right < left; (* ASSERT (FOR i IN {low..right} : array [i] <= separator); *) (* ASSERT (FOR i IN {left..high} : separator <= array [i]); *) IF low < right THEN SortBetween (low, right); END; IF left < high THEN SortBetween (left, high); END; ASSERT (SortedBetween (array, low, high), BEC.postcondition); END SortBetween; BEGIN SortBetween (0, LEN (array) - 1); ASSERT (Sorted (array), BEC.postcondition); END Quicksort; END I3ToolsetForArrayOfInteger. Da in Component Pascal stets LEN (array) > 0 ist, gilt bei SortBetween die Vorbedingung low <= high, und als Schleife lässt sich die fußgesteuerte REPEAT-Schleife einsetzen. In den anderen Sprachen trifft dies nicht zu. Da die beiden rekursiven Aufrufe von SortBet27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 24 4 Reihungen und Abstraktionen aufeinander folgen, ist SortBetween nicht endrekursiv, sodass das Umformungsschema von S. 3-5 nicht anwendar ist. ween 4.2.2 Eiffel Aufgabe 4.6 Eiffel: Quicksort, rekursiv Entwickeln Sie eine Eiffel-Variante zu dieser Problemlösung als Teil der generischen Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE von Programm 4.4! 4.2.3 C++ Ein generisches rekursives Quicksort zum Sortieren einer Reihung sieht in C++ so aus: Programm 4.14 C++: Quicksort, rekursiv, Schnittstelle mit generischen Funktionen // Header file: ToolsetArrayComparableM.hpp #ifndef TOOLSET_ARRAY_COMPARABLE_M_HPP_ #define TOOLSET_ARRAY_COMPARABLE_M_HPP_ #include <cassert> #include <vector> namespace ToolsetArrayComparable { // Other stuff here. template <typename T> void sort_between (std::vector<T> & array, int low, int high) // Instantiation condition: T is a comparable type. { assert(0 <= low && low < high && high < array.size()); // precondition int left = low, right = high; T separator = array[(low + high) / 2]; do { while (array[left] < separator) ++left; while (separator < array[right]) --right; if (left <= right) { T store = array[left]; array[left] = array[right]; array[right] = store; ++left; --right; } } while (left <= right); if (low < right) sort_between(array, low, right); if (left < high) sort_between(array, left, high); assert(sorted_between(array, low, high)); // postcondition }s template <typename T> void quicksort (std::vector<T> & array) { if (array.size() > 1) sort_between(array, 0, array.size() - 1); assert(sorted(array)); // postcondition } } // ToolsetArrayComparable #endif // TOOLSET_ARRAY_COMPARABLE_M_HPP_ Aufgabe 4.7 C++: Quicksort, rekursiv mit generischer Klasse Schreiben Sie die generischen Funktionen von Programm 4.14 um zu Klassenfunktionen der generischen Klasse ToolsetArrayComparable in der Schnittstellendatei ToolsetArrayComparableC.hpp von Programm 4.7! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.3 Indexbereich mit maximaler Summe 4 – 25 4.2.4 Java Aufgabe 4.8 Java: Quicksort, rekursiv Entwickeln Sie eine Java-Variante zu dieser Problemlösung als Teil der generischen Werkzeugkastenklasse ToolsetArrayComparable von Programm 4.10! 4.2.5 C# Aufgabe 4.9 C#: Quicksort, rekursiv Entwickeln Sie eine C#-Variante zu dieser Problemlösung als Teil der generischen Werkzeugkastenklasse ToolsetArrayComparable! 4.2.6 Fazit Zu ergänzen. 4.3 Indexbereich mit maximaler Summe Die Lösung des folgenden Problems ist beispielsweise für Urlauber und Börsenspekulanten interessant. Der Urlauber will wissen, wann das Wetter am schönsten ist. Der Spekulant will wissen, an welchen Tagen er bestimmte Aktien kaufen und verkaufen soll, um maximalen Gewinn zu erzielen. Die Lösungen setzen allerdings voraus, dass die Wetterdaten bzw. täglichen Aktienkursschwankungen schon bekannt sind. Problem Maximalsumme Gegeben: Eine Reihung array ganzer oder reeller Zahlen. Gesucht: Ein zusammenhängender Indexbereich low,.., high, sodass die Summe der Werte array[low],.., array[high] maximal wird. Es gibt stets eine Lösung, manchmal mehrere Lösungen. Verlangt man z.B. zusätzlich den Indexbereich mit größtem unterem Index low und dann dem kleinstem oberen Index high mit low ≤ high, so ist die Lösung eindeutig und es handelt sich mathematisch wie bei den Problemen 3.1 S. 3-1 und 4.1 um eine Abbildung, etwa range_with_max_sum : n → 2 × . Als Zahlentyp wählen wir im Folgenden exemplarisch Ganzzahlen. Eine passende Lösung ist eine Prozedur mit der Reihung als Eingabeparameter und drei Ausgabeparametern für die beiden Indizes und die maximale Summe. Da nicht alle Auswahlsprachen Ausgabeparameter kennen, sind alternative Lösungen zu suchen. 4.3.1 Ersetzen von Ausgabeparametern Als Alternative zur prozeduralen Lösung mit Ausgabeparametern stellen wir eine objektorientierte Lösung nach dem Aktion-Abfrage-Schema vor, bei dem Ausgabeparameter durch Attribute oder Datenelemente der Klasse ersetzt sind. Es handelt sich um die objektorientierte Variante des konventionellen Ersetzens von Referenzparametern durch globale Variablen. Frühe Programmiersprachen kannten nur parameterlose Prozeduren; diese mussten über globale Variablen kommunizieren. Tabelle 4.1 zeigt exemplarisch Umformungen von Codefragmenten in Eiffel-ähnlichem Pseudocode mit Eingabe- (in) und Ausgabe- (out) Parametern in äquivalente Fragmente, die ohne Parameter auskommen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 26 Tabelle 4.1 Ersetzen von Parametern durch globale Variablen 4 Reihungen und Abstraktionen Parameterübergabe Äquivalentes Programm ohne Parameterübergabe x : INT p (in x : INT) : INT is do result := x * x end p : INT is do result := x * x end q is local a : INT do a := p (2) end q is local a : INT do x := 2 a := p end x : INT 4.3.2 p (out x : INT) is do x := 1 end p is do q is local a : INT do p (a) end q is local a : INT do p a := x end x := 1 end Component Pascal Die Component-Pascal-Variante realisieren wir als Prozedur mit Ausgabeparametern. Der effiziente Algorithmus stammt aus D. Gries: A Note on the Standard Strategy for Developing Loopinvariants and Loops, Science of Computer Programming 2, S. 207214. Gibt es mehrere Lösungen, so liefert er irgendeine. Der Algorithmus wird auch als Abtastalgorithmus (scan-line-algorithm) bezeichnet. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.3 Indexbereich mit maximaler Summe Programm 4.15 Component Pascal: Indexbereich mit maximaler Summe 4 – 27 MODULE I3ToolsetForArrayOfInteger; (* Other stuff here. *) PROCEDURE DetermineRangeWithMaxSum* (IN array : ARRAY OF Numeric; OUT maxLow, maxHigh : Index; OUT maxSum : Numeric); (*! Postcondition: 0 <= maxLow <= maxHigh < LEN (array). maxLow, maxHigh such that array [maxLow] + ... + array [maxHigh] = maximal. maxSum = array [maxLow] + ... + array [maxHigh]. !*) VAR low, high : Index; sum : Numeric; BEGIN low := 0; sum := array [low]; maxLow := low; maxHigh := low; maxSum := sum; FOR high := low + 1 TO LEN (array) - 1 DO IF sum <= 0 THEN low := high; sum := 0; END; INC (sum, array [high]); IF sum > maxSum THEN maxLow := low; maxHigh := high; maxSum := sum; END; END; END DetermineRangeWithMaxSum; END I3ToolsetForArrayOfInteger. 4.3.3 Eiffel Verallgemeinern In Eiffel sehen wir eine neue generische Werkzeugkastenklasse TOOLSET_ARRAY_COMPARABLE_NUMERIC vor. Das Problem ist mit einem generischen Elementtyp E lösbar, für den eine Vollordnung und eine Addition definiert sind. Daher genügt es zu fordern, dass E von den Standardbibliotheksklassen COMPARABLE (für die Ordnungsrelationen) und NUMERIC (für die arithmetischen Operationen) erbt, d.h. der formale generische Parameter wird durch eine mehrfache Vererbungsbeziehung eingeschränkt:1 E -> {COMPARABLE, NUMERIC} Codevererbung Da mit Reihungen vergleichbarer numerischer Elemente alles machbar ist, was mit Reihungen vergleichbarer Elemente machbar ist, lassen wir TOOLSET_ARRAY_COMPARABLE_NUMERIC von TOOLSET_ARRAY_COMPARABLE erben – ein Beispiel für Codevererbung: Beide Klassen sind konkret, d.h. vollständig implementiert; TOOLSET_ARRAY_COMPARABLE_NUMERIC übernimmt alle geerbten Merkmale ohne Redefinitionen. Ausgabeparameter ersetzen Die Kommunikation modellieren wir nach dem Kommando-Abfrage-Schema: Die Ausgabeparameter der Prozedur der Component-Pascal-Variante ersetzen wir durch 1 Dieses Sprachmerkmal ist im ECMA-Standard [ECMA367] in 8.12.8 enthalten, aber vielleicht nicht in jeder Sprachimplementation (Zugriff 2006-04-07). 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 28 4 Reihungen und Abstraktionen entsprechende Attribute. Die Prozedur reduziert sich auf ein parameterloses Kommando. Der Effekt eines Kommandoaufrufs lässt sich nach dem Aufruf durch Abfragen der Attribute ermitteln. Bild 4.2 Klassendiagramm zu Werkzeugkastenklasse n TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE] query sorted has command gapsort quicksort TOOLSET_ARRAY_COMPARABLE_NUMERIC [E -> {COMPARABLE, NUMERIC}] Trennen von Abfragen und Kommandos Programm 4.16 Eiffel: Indexbereich mit maximaler Summe query max_low max_high max_sum command determine_range_with_max_sum Unsere Klassendiagramme sind nicht konform zur UML. Indem wir zwischen Abfragen und Kommandos unterscheiden, betonen wir einen an Klassenschnittstellen wichtigen semantischen Aspekt. Demgegenüber ist die Unterscheidung zwischen Attributen und Methoden ein Implementationsaspekt, der beim Entwurf von Klassenschnittstellen keine Rolle spielen sollte. Attribute, die nicht schreibgeschützt exportiert werden können, sollten nicht zu Schnittstellen gehören. Einer parameterlosen Abfrage ist dagegen nicht anzusehen, ob sie als Attribut oder Funktion implementiert wird. Dies ist ein Aspekt der Bezugstranparenz (siehe S. 1-10). note description: "Tools operating on arrays of comparable numeric elements" class TOOLSET_ARRAY_COMPARABLE_NUMERIC [E -> {COMPARABLE, NUMERIC}] inherit TOOLSET_ARRAY_COMPARABLE [E] feature max_low, max_high : INTEGER max_sum : E © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.3 Indexbereich mit maximaler Summe 4 – 29 determine_range_with_max_sum (array : ARRAY [E]) require array_exists: array /= Void local low, high : INTEGER sum : E do from low := array.lower high := low + 1 sum := array.item (low) max_low := low max_high := low max_sum := sum until high > array.upper loop if sum <= zero then low := high sum := zero end sum := sum + array.item (high) if sum > max_sum then max_low := low max_high := high max_sum := sum end high := high + 1 end ensure low_large_enough: array.lower <= max_low range_valid: max_low <= max_high high_small_enough: max_high <= array.upper -- range_has_maximal_sum: max_low, max_high such that -array.item (max_low) + ... + array.item (max_high) = maximal -- maximal_sum: max_sum = array.item (max_low) + ... + array.item (max_high) end -- determine_range_with_max_sum end -- class TOOLSET_ARRAY_COMPARABLE_NUMERIC Implizite Initialisierung TOOLSET_ARRAY_COMPARABLE_NUMERIC braucht keine Erzeugungsprozedur, weil Attribute max_low, max_high, max_sum erst nach einem Aufruf von determine_range_with_max_sum definierte Werte haben müssen. Bei der Objekterzeu- die gung werden die Attribute implizit mit Standardwerten – konkret mit Nullen – initialisiert. Aufgabe 4.10 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe Entwickeln Sie einen Testtreiber zur Eiffel-Variante dieses Problems! 4.3.4 C++ Erben oder Inkludieren? Da wir in C++ für Ausgabeparameter Referenzen übergeben können, verzichten wir auf das Aktion-Abfrage-Schema. Für die erste C++-Variante sehen wir eine neue Schnittstellendatei ToolsetArrayComparableNumericM.hpp vor. Da mit Reihungen vergleichbarer numerischer Elemente alles machbar ist, was mit Reihungen vergleichbarer Elemente machbar ist, inkludieren wir ToolsetArrayComparableM.hpp in ToolsetArray- 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 30 4 Reihungen und Abstraktionen ComparableNumericM.hpp. Somit benutzen wir Inkludierung als primitive Form der Codevererbung. Programm 4.17 C++: Indexbereich mit maximaler Summe, Schnittstelle // Header file: ToolsetArrayComparableNumericM.hpp #ifndef TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_ #define TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_ #include <cassert> #include <vector> #include "ToolsetArrayComparableM.hpp" namespace ToolsetArrayComparable { template <typename T> void determine_range_with_max_sum (const std::vector<T> & array, int & max_low, int & max_high, T & max_sum) // Instantiation condition: T is a comparable numeric type. { int low = 0; T sum = array[low]; max_low = low; max_high = low; max_sum = sum; for (int high = low + 1; high < array.size(); ++high) { if (sum <= 0) { low = high; sum = 0; } sum += array[high]; if (sum > max_sum) { max_low = low; max_high = high; max_sum = sum; } } assert(0 <= max_low); // postcondition assert(max_low <= max_high); // postcondition assert(max_high < array.size()); // postcondition } } // ToolsetArrayComparable #endif // TOOLSET_ARRAY_COMPARABLE_NUMERIC_M_HPP_ Aufgabe 4.11 C++: Indexbereich mit maximaler Summe als Klassenfunktion Entwickeln Sie eine C++-Variante dieses Problems mit determine_range_with_max_sum als Klassenfunktion einer generischen Klasse ToolsetArrayComparableNumeric, die von ToolsetArrayComparable von Programm 4.7 erbt! Aufgabe 4.12 C++: Testtreiber zu Indexbereich mit maximaler Summe Entwickeln Sie Testtreiber zu den C++-Varianten dieses Problems! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.3 Indexbereich mit maximaler Summe 4.3.5 4 – 31 Java Da Java keine Ausgabeparameter kennt, wenden wir das in der Eiffel-Variante demonstrierte Aktion-Abfrage-Schema an. Programm 4.18 Java: Indexbereich mit maximaler Summe public class ToolsetArrayInteger { /* Other stuff here. */ private int maxLow, maxHigh; private int maxSum; public int maxLow () { return maxLow; } public int maxHigh () { return maxHigh; } public int maxSum () { return maxSum; } public void determineRangeWithMaxSum (int[] array) { assert array != null : "precondition"; int low = 0; int sum = array[low]; maxLow = low; maxHigh = low; maxSum = sum; for (int high = low + 1; high < array.length; ++high) { if (sum <= 0) { low = high; sum = 0; } sum += array[high]; if (sum > maxSum) { maxLow = low; maxHigh = high; maxSum = sum; } } assert 0 <= maxLow() : "postcondition"; assert maxLow() <= maxHigh() : "postcondition"; assert maxHigh() < array.length : "postcondition"; } } Datenfelder und Funktionen sind nicht static vereinbart, damit das Aktion-AbfrageMuster flexibel nutzbar ist. Aufgabe 4.13 Java: Indexbereich mit maximaler Summe Entwickeln Sie eine Java-Variante dieses Problems mit einer generischen Klasse ToolsetArrayComparableNumeric, die von ToolsetArrayComparable von Programm 4.10 erbt! Entwickeln Sie auch einen Testtreiber dazu! 4.3.6 C# Aufgabe 4.14 C#: Indexbereich mit maximaler Summe Entwickeln Sie eine C#-Variante zu dieser Problemlösung! Entwickeln Sie auch einen Testtreiber dazu! 4.3.7 Fazit Zu ergänzen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 32 4.4 4 Reihungen und Abstraktionen Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen Bisher haben wir zu den Problemen 4.1 bis 4.3 jeweils einzelne Testtreiber geschrieben. Die Copy-&-Paste-Methode führt zwar schnell zu Lösungen, repliziert aber Quellcode, was die Wartbarkeit der Testtreiber reduziert. Ein besserer Entwurf der Testtreiber nutzt statt Codereplikation die Abstraktionsmechanismen Benutzung, Vererbung und Generizität. Dazu analysieren wir die einfachen Testtreiber, um gemeinsame Teile zu erkennen und in geeignete abstrakte generische Klassen heraus zu faktorisieren. Die Aufgaben, die die Testtreiber zu erledigen haben, gliedern wir teilweise horizontal arbeitsteilig, teilweise vertikal nach allgemeinen und speziellen Teilaufgaben: Als ausgliederbare Teilaufgabe bietet sich die Initialisierung von Reihungen mit Zufallswerten an. Das ausgegliederte Teil, der Initialisierer, wird weiter vertikal mittels Vererbung und Generizität zerlegt. Die Testtreiber delegieren das Initialisieren von Reihungen an den Initialisierer und benutzen einen Werkzeugkasten. So bleiben ihnen sonst nur die Aufgaben, Kommandoargumente zu übernehmen und Testergebnisse auszugeben. Auch die Testtreiber gliedern wir vertikal mittels Vererbung und Generizität. 4.4.1 Eiffel Die folgenden Klassendiagramme stellen den Entwurf der Eiffel-Variante dar. Dieser lässt sich auf Entwurfsvarianten anderer Sprachen mit Generizität übertragen. Ohne Generizität muss man freilich auf gewisse Abstraktionen verzichten. Merkmale in der linken Spalte sind öffentlich, Merkmale in der rechten Spalte geschützt. 4.4.1.1 Initialisierer Bild 4.3 Klassendiagramm zur Initialisierung von Reihungen INITIALIZER_OF_ARRAY [E -> COMPARABLE] abstract query min max command init_random make random random_item abstract E = STRING INITIALIZER_OF_ARRAY_STRING random_item query E = REAL INITIALIZER_OF_ARRAY_REAL query random_item Die Initialisiererklasse INITIALIZER_OF_ARRAY soll einerseits flexibel Reihungen mit beliebigen Elementtypen initialisieren können, andererseits weit gehend fertig implementiert sein, d.h. es sollen möglichst wenig Implementationen fehlen. Zwecks Variation des Elementtyps ist die Klasse generisch, zwecks Offenlassen von Implementationen abstrakt. Schnittstellen- und Implementationsklasse INITIALIZER_OF_ARRAY ist eine Schnittstellenklasse, d.h. sie bietet eine vollständige Schnittstelle, die unvollständig implementiert ist. Hier ist nur die Abfrage random_item abstrakt, also noch nicht implementiert. Nachfolgerklassen von INITIALIZER_OF_ARRAY konkretisieren den generischen Typ, können dadurch random_item implementieren und werden so zu Implementationsklassen, die nur die geerbte Schnittstelle implementieren, ohne sie durch weitere Merkmale zu erweitern. Solche Implementationsklassen eignen sich zur polymorphen Benutzung. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen Programm 4.19 Eiffel: Abstrakte generische Initialisierung von Reihungen vergleichbarer Elemente 4 – 33 note description: "Abstract array initialization using the template method design pattern" deferred class INITIALIZER_OF_ARRAY [E -> COMPARABLE] feature {NONE} random : RANDOM once create Result.make ensure random_exists: Result /= Void end -- random item : E forth -- Give item another random value. deferred end -- forth feature min : E max : E init_random (array : ARRAY [E]) -- Initialize array with random values between min and max. require array_exists: array /= Void local index : INTEGER do from index := array.lower until index > array.upper loop array.put (item, index) forth index := index + 1 end end -- init_random 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 34 4 Reihungen und Abstraktionen make (new_min, new_max : E) require new_min_exists: new_min /= Void new_max_exists: new_max /= Void new_bounds_consistent: new_min <= new_max do min := new_min max := new_max forth ensure min_defined: min = new_min max_defined: max = new_max end -- make invariant random_exists: min_exists: max_exists: item_exists: item_large_enough: item_small_enough: random /= Void min /= Void max /= Void item /= Void min <= item item <= max end -- class INITIALIZER_OF_ARRAY Abstrakte Klassen heißen in Eiffel aufgeschoben (deferred) und werden mit deferred markiert. Entsprechend tritt bei abstrakten Routinen an die Stelle des mit do eingeleiteten Anweisungsteils das deferred. Entwurfsmuster Schablonenmethode Die abstrakte generische Klasse INITIALIZER_OF_ARRAY erfüllt ihre Aufgabe, Reihungen mit Zufallswerten zu initialisieren, mit der Routine init_random, die komplett implementiert ist. init_random benutzt allerdings die abstrakte Prozedur forth. Im Katalog der objektorientierten Entwurfsmuster heißt eine vollständig implementierte Methode, die abstrakte Methoden aufruft, Schablonenmethode (template method) [GHJV04]. forth gibt item einen Zufallswert des generischen Typs E zwischen den Grenzen min und max. Zwecks flexibler Benutzbarkeit übernimmt die Erzeugungsprozedur make die Bereichsgrenzen als Parameter und speichert sie. forth kann ein durch die Einmalfunktion random erzeugtes RANDOM-Objekt benutzen. Nachfolgerklassen von INITIALIZER_OF_ARRAY müssen nur noch forth implementieren. Programm 4.20 Eiffel: Initialisierung gleitpunktzahliger Reihungen note description: "Real array initialization" class INITIALIZER_OF_ARRAY_REAL inherit INITIALIZER_OF_ARRAY [REAL] create make feature {NONE} forth do item := ((random.item - 1) / (random.modulus - 2)) * (max - min) + min end -- forth end -- class INITIALIZER_OF_ARRAY_REAL Die Implementationsklasse INITIALIZER_OF_ARRAY_REAL erbt von INITIALIZER_OF_ARRAY und konkretisiert den generischen Parameter E zu REAL. Im Vererbungsabschnitt ist anzugeben, dass forth redefiniert wird. Die Implementation von © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen 4 – 35 forth ähnelt der von random_item in Programm 4.5. Mehr Aufwand fordert die Implementationsklasse INITIALIZER_OF_ARRAY_STRING, die Reihungen mit zufällig langen zufälligen Zeichenketten initialisieren soll. Programm 4.21 Eiffel: Initialisierung von Reihungen von Zeichenketten note description: "String array initialization" class INITIALIZER_OF_ARRAY_STRING inherit INITIALIZER_OF_ARRAY [STRING] create make feature {NONE} max_string_capacity : INTEGER once Result := min.capacity.max (max.capacity) end -- Max_string_capacity max_value : INTEGER = 61 -- = 2 * 26 + 10 - 1, but constant expression not allowed here! random_alpha_numeric : CHARACTER local value : INTEGER do value := random.item \\ (max_value + 1) inspect value when 0 .. 9 then Result := (value + ('0').code).to_character when 10 .. 35 then Result := (value + ('A').code - 10).to_character when 36 .. max_value then Result := (value + ('a').code - 36).to_character else check unreachable_branch: False end end ensure ok: Result.is_alpha or Result.is_digit end -- random_alpha_numeric 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 36 4 Reihungen und Abstraktionen forth local i : INTEGER do from random.forth create item.make_filled ('?', random.item \\ max_string_capacity + 1) until min <= item and item <= max loop -- Danger of infinite loop! from i := item.index_set.lower until i > item.index_set.upper loop random.forth item.put (random_alpha_numeric, i) i := i + 1 end end end -- forth end -- class INITIALIZER_OF_ARRAY_STRING forth soll für item eine zufällig lange zufällige Zeichenkette produzieren. Zunächst erzeugt forth eine mit '?' gefüllte Zeichenkette, die höchstens so lang wie die beschränkenden Zeichenketten min und max ist. Dann ersetzt es die '?' mit random_alpha_numeric durch zufällige alphanumerische Zeichen. Da die produzierte Zeichenkette nicht sicher durch min und max beschränkt ist, wiederholt forth das Ersetzen. Je näher die Schranken beieinander liegen, desto länger kann es dauern, bis eine korrekte Zeichenkette produziert ist – vielleicht ewig. Bessere Algorithmen sind möglich. Mehrseitige Auswahl random_alpha_numeric wandelt die aktuelle Zufallszahl in eine Ziffer, einen Klein- oder Großbuchstaben. Dazu benutzt es die mehrseitige Auswahlanweisung, die in Eiffel mit dem Schlüsselwort inspect beginnt. Wie die CASE-Anweisung von Component Pascal entspricht die inspect-Anweisung der 1-Eingang-/1-Ausgang-Regel strukturierten Programmierens und erlaubt wie jene, mehrere Fälle durch Aufzählungen und Intervalle zusammenzufassen. Doch während die CASE-Anweisung bei Fällen konstante Ausdrücke zulässt, dürfen in der inspect-Anweisung dort nur Konstanten vorkommen, z.B. ist das Fallintervall when 36 .. number_of_alpha_numeric_chars - 1 then illegal. Dieser Nachteil folgt aus der allgemeinen Schwäche von Eiffel, Konstanten nicht gut zu unterstützen. Beispielsweise ist die Konstantenvereinbarung number_of_alpha_numeric_chars : INTEGER = 2 * 26 + 10 nicht möglich, da rechts nur Konstanten, keine konstanten Ausdrücke erlaubt sind. Der Programmierer kann entweder gegen die Programmierleitlinie, Abhängigkeiten explizit auszudrücken, verstoßen und selbst ausrechnen, was sie dem Kompilierer überlassen will, oder auf die Konstante verzichten und ihren Wert mit einer Einmalfunktion implementieren: number_of_alpha_numeric_chars : INTEGER once Result := 2 * 26 + 10 end © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen 4.4.1.2 4 – 37 Testtreiber Bild 4.4 Klassendiagramm zur Testtreibern TESTER_OF_TSAC [E -> COMPARABLE] abstract query tsac array ia command make abstract create_ia abstract init E = STRING TESTER_OF_TSAS abstract TESTER_OF_TSAR abstract tsac query tsac command create_ia command create_ia command make INITIALIZER_OF_ARRAY [E -> COMPARABLE] E = REAL query TESTER_OF_TSAS_HAS TOOLSET_ARRAY_COMPARABLE [E -> COMPARABLE] TESTER_OF_TSAR_DETERMINE command make Die Testerklasse TESTER_OF_TSAC soll einerseits flexibel verschiedene Werkzeuge für Reihungen mit beliebigen Elementtypen testen können, andererseits fast fertig implementiert sein. Wie die Initialisiererklasse ist sie zur Variation des Elementtyps generisch, zum Offenlassen von Implementationen abstrakt. Sie wird aber über zwei Stufen konkretisiert: Der abstrakte generische Testtreiber TESTER_OF_TSAC ist eine Schnittstellenklasse. Sie benutzt die allgemeinere generische Werkzeugkastenklasse und die abstrakte generische Initialisiererklasse. Da der Elementtyp E generisch ist, kann hier kein Initialisiererobjekt erzeugt werden. Deshalb ist dafür die abstrakte Prozedur create_ia vorgesehen. Offen bleibt, welches Werkzeug zu testen ist. Dafür ist die abstrakte Prozedur make vorgesehen. Der erste Konkretisierungsschritt liefert eine Schnittstellenklasse mit konkretisiertem Elementtyp E, z.B. TESTER_OF_TSAS für STRING. Damit ist create_ia implementierbar. Außerdem lässt sich der Typ des benutzten Werkzeugkastenobjekts tsac genau festlegen. Der zweite Konkretisierungsschritt liefert eine Implementationsklasse, die das getestete Werkzeug festlegt, z.B. TESTER_OF_TSAS_HAS die binäre Suchfunktion has. Sie implementiert make mit einem geeigneten Algorithmus. Programm 4.22 Eiffel: Abstrakter generischer Testtreiber zu Reihungen vergleichbarer Elemente 27.9.12 note description: "Abstract generic test driver for class TOOLSET_ARRAY_COMPARABLE" deferred class TESTER_OF_TSAC [E -> COMPARABLE] feature {NONE} tsac : TOOLSET_ARRAY_COMPARABLE [E] array : ARRAY [E] ia : INITIALIZER_OF_ARRAY [E] © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 38 4 Reihungen und Abstraktionen create_ia (min, max : E) require min_exists: min /= Void max_exists: max /= Void bounds_consistent: min <= max deferred ensure ia_exists: ia /= Void end -- create_ia init (size : INTEGER; min, max : E) require size_positive: size > 0 min_exists: min /= Void max_exists: max /= Void bounds_consistent: min <= max do create tsac create array.make (1, size) create_ia (min, max) ia.init_random (array) io.put_string ("array: "); print (array) ensure tsac_exists: tsac /= Void array_exists: array /= Void ia_exists: ia /= Void end -- init feature make deferred end -- make end -- class TESTER_OF_TSAC Erläuterungen zu ergänzen. Programm 4.23 Eiffel: Abstrakter Testtreiber zu Reihungen von Zeichenketten note description: "Abstract test driver for type TOOLSET_ARRAY_COMPARABLE [STRING]" deferred class TESTER_OF_TSAS inherit TESTER_OF_TSAC [STRING] feature {NONE} create_ia (min, max : STRING) do create {INITIALIZER_OF_ARRAY_STRING} ia.make (min, max) end -- create_ia end -- class TESTER_OF_TSAS Die Initialisierergröße ia soll polymorph benutzt werden und behält daher ihren abstrakten Typ INITIALIZER_OF_ARRAY [E], der zu INITIALIZER_OF_ARRAY [STRING] konkretisiert ist. Das redefinierte Kommando create_ia erzeugt mit der modifizierten create-Anweisung ein Objekt des passenden konkreten Typs INITIALIZER_OF_ARRAY_STRING. Analog wird create_ia in der REAL-Variante redefiniert. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen Programm 4.24 Eiffel: Abstrakter Testtreiber zu gleitpunktzahligen Reihungen 4 – 39 note description: "Abstract test driver for type TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL]" deferred class TESTER_OF_TSAR inherit TESTER_OF_TSAC [REAL] redefine tsac end feature {NONE} tsac : TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL] create_ia (min, max : REAL) do create {INITIALIZER_OF_ARRAY_REAL} ia.make (min, max) end -- create_ia end -- class TESTER_OF_TSAR Die REAL-Variante redefiniert zusätzlich das mit einem Werkzeugkastenobjekt zu verbindende tsac. Diese Redefinition ist nur eine konforme Typänderung. Da der aktuelle generische Typ REAL vergleichbar und numerisch ist, kann das Werkzeugkastenobjekt anstelle des ursprünglich vorgesehenen Typs TOOLSET_ARRAY_COMPARABLE [E] den erweiterten Typ TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL] annehmen. Damit bietet tsac die entsprechend erweiterte Schnittstelle. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 40 Programm 4.25 Eiffel: Testtreiber zu binärem Suchen in Reihungen von Zeichenketten 4 Reihungen und Abstraktionen note description: "Test driver for feature TOOLSET_ARRAY_COMPARABLE [STRING].has" class TESTER_OF_TSAS_HAS inherit TESTER_OF_TSAS create make feature make -- Given one integer and three string command arguments with the meaning -- 1. size > 0, -- 2. min, -- 3. max with min <= max, -- 4. item, -- fill a string array of size size with random values between min and max and -- check if it contains item. local command_line : ARGUMENTS size : INTEGER min, max, item : STRING do create command_line if command_line.argument_count < 4 or else not command_line.argument (1).is_integer or else command_line.argument (1).to_integer <= 0 or else command_line.argument (2) > command_line.argument (3) then io.put_string ("Please call this command with 1 integer, 3 string arguments") io.put_string (" size (>0), min, max (>= min), item!") else size := command_line.argument (1).to_integer min := command_line.argument (2) max := command_line.argument (3) item := command_line.argument (4) init (size, min, max) tsac.sort (array) io.put_string (" item " + item) if not tsac.has (array, item) then io.put_string (" not") end io.put_string (" found in array.") end io.put_new_line end -- make end -- class TESTER_OF_TSAS_HAS Im Unterschied zum ursprünglichen einfachen Testtreiber TESTER_OF_TSACI_HAS Programm 4.5 befasst sich TESTER_OF_TSAS_HAS fast nur noch mit Ein-/Ausgabe; sein funktionaler Teil beschränkt sich auf die Aufrufe init (size, min, max), tsac.sort (array) und tsac.has (array, item). Um andere Testtreiber zu erhalten, können wir auf die Copy-&-Paste-Methode nicht ganz verzichten. Für Testtreiber für Reihungen mit anderen Elementtypen sind an Programm 4.25 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen 4 – 41 die Vererbungsbeziehung, das Prüfen und Konvertieren der Kommandoargumente und das Ausgeben der Ergebnisse anzupassen. Zum Testen anderer Merkmale der Werkzeugkastenklassen sind die Aufrufe dieser Merkmale anzupassen. Exemplarisch zeigen wir einen Testtreiber für den Indexbereich mit maximaler Summe. Programm 4.26 Eiffel: Testtreiber zu Indexbereich mit maximaler Summe in gleitpunktzahligen Reihungen note description: "Test driver for feature TOOLSET_ARRAY_COMPARABLE_NUMERIC [REAL].determine_range_with_max_sum" class TESTER_OF_TSAR_DETERMINE inherit TESTER_OF_TSAR create make feature make -- Given an integer and two real command arguments with the meaning -- 1. size > 0, -- 2. min < 0 -- 3. max with min <= max, -- fill a real array of size size with random values between min and max and -- determine a range with maximal sum. local command_line : ARGUMENTS size : INTEGER min, max : REAL do create command_line if command_line.argument_count < 3 or else not command_line.argument (1).is_integer or else command_line.argument (1).to_integer <= 0 or else not command_line.argument (2).is_real or else not command_line.argument (3).is_real or else command_line.argument (2).to_real > command_line.argument (3).to_real then io.put_string ("Please call this command with an integer and two real") io.put_string (" arguments size (>0), min, max (>= min)!") else size := command_line.argument (1).to_integer min := command_line.argument (2).to_real max := command_line.argument (3).to_real init (size, min, max) tsac.determine_range_with_max_sum (array) io.put_string (" max_low: "); io.put_integer (tsac.max_low) io.put_string (" max_high: "); io.put_integer (tsac.max_high) io.put_string (" max_sum: "); io.put_real (tsac.max_sum) end io.put_new_line end -- make end -- class TESTER_OF_TSAR_DETERMINE 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 42 4.4.2 4 Reihungen und Abstraktionen C++ Prinzipiell kann man für die C++-Variante den in Bild 4.3 dargestellten Entwurf der Initialisiererklassen und den in Bild 4.4 dargestellten Entwurf der Testtreiber übernehmen – es ist aber ein Stück harter Arbeit im Detail. Deshalb sei diese Aufgabe dem interessierten Leser zur Übung überlassen. Hier vereinfachen wir den Entwurf in folgenden Aspekten, wobei wir freilich einige Abstraktionen verlieren: Initialisiererklasse Testerklasse tsac ia, create_ia make create_ia, init Testerhauptprogramm Eine statt drei Initialisiererklassen: Das bietet sich solange an, wie wir uns bei den exemplarischen Testtreibern auf die Typen int und float beschränken. Die C++-Initialisiererklasse InitializerOfArray ist dann wie die Eiffel-Initialisiererklasse INITIALIZER_OF_ARRAY generisch, aber nicht abstrakt, weil wir random_item sofort implementieren, wobei die Implementation eine explizite Typkonversion in den generischen Typ verwendet. Typkonversionen sind in Eiffel verpönt, in C/C++ beliebt. Die Implementation von random_item passt zu den Typen int und float, aber freilich nicht zu string. So sparen wir die Definition von zwei Ableitungsklassen (auf Kosten der Chance, abstrakte Klassen und Vererbung in C++ kennen zu lernen). Falls die Standardimplementation von random_item zu einem aktuellen generischen Typ nicht passt, so ist random_item in einer Ableitungsklasse passend zu redefinieren oder besser eine Abstraktionsklasse einzuführen. Die Allgemeinheit des Entwurfs ist also nicht eingeschränkt. Eine konkrete statt drei abstrakte Testerklassen: Die C++-Testerklasse TesterOfTSAC ist wie die Eiffel-Testerklasse TESTER_OF_TSAC generisch, aber aus mehreren Gründen nicht abstrakt: Bei der modularen Variante des Werkzeugkastens entfällt die Notwendigkeit, ein passendes Objekt tsac zu erzeugen. tsac wird zu einem Aliasnamen für den Namensraum des Werkzeugkastens. Solange es nur eine konkrete Initialisiererklasse gibt, entfällt die Notwendigkeit der polymorphen Erzeugung eines passenden Initialisiererobjekts ia. Statt des abstrakten create_ia mit zwei Konkretisierungen genügt ein konkretes create_ia. Somit können wir auf die abstrakten Testerklassen TESTER_OF_TSAS und TESTER_OF_TSAR verzichten. Sodann vereinfachen wir weiter: Da in C++ Konstruktoren nicht abstrakt sein können, ist das abstrakte make sinnlos und entfällt. Da man in C++ bei Klassen mit Datenfeldern kaum auf Konstruktoren verzichten kann, lassen wir die Aufgaben des nun konkreten create_ia und des schon konkreten init von einem Konstruktor erledigen. Die konkreten Eiffel-Testerklassen TESTER_OF_TSAS_HAS und TESTER_OF_TSAR_DETERMINE erledigen ihre Aufgaben vollständig in ihren Erzeugungsprozeduren make. Da C++ nicht ohne main-Funktion auskommt, ist gemäß diesem Entwurf in einem main ein Objekt einer konkreten Testerklasse zu erzeugen, dessen Konstruktor die eigentliche Arbeit leistet. Dies ist zwar gängige Praxis, aber umständlich, weil man für jeden Test eine Klasse mit einem Konstruktor und das unvermeidliche main schreiben muss. Deshalb verzichten wir auf konkrete Testerklassen und lassen ihre Aufgaben direkt von main-Funktionen erledigen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen 4.4.2.1 Programm 4.27 C++: Generische Initialisierung von Reihungen vergleichbarer Elemente 4 – 43 Initialisierer // Header file: InitializerOfArray.hpp #ifndef INITIALIZER_OF_ARRAY_HPP_ #define INITIALIZER_OF_ARRAY_HPP_ #include <cassert> #include <cstdlib> #include <iostream> #include <vector> template <typename T> class InitializerOfArray // Instantiation condition: T is a comparable type. { protected: virtual T random_item (); T min, max; public: T get_min () {return min;} T get_max () {return max;} virtual void init_random (std::vector<T> & array); virtual void print (const std::vector<T> & array); InitializerOfArray (T new_min, T new_max); }; // InitializerOfArray template <typename T> T InitializerOfArray<T>::random_item () { T result = T((double(std::rand() - 1) / (RAND_MAX - 1)) * (max - min + 1) + min); assert(get_min() <= result); // postcondition assert(result <= get_max()); // postcondition return result; } template <typename T> void InitializerOfArray<T>::init_random (std::vector<T> & array) { for (int index = 0; index < array.size(); ++index) { array[index] = random_item(); } } template <typename T> void InitializerOfArray<T>::print (const std::vector<T> & array) { std::cout << "["; for (int index = 0; index < array.size(); ++index) { std::cout << array[index] << " "; } std::cout << "]" << std::endl; } template <typename T> InitializerOfArray<T>::InitializerOfArray (T new_min, T new_max) { assert(new_min <= new_max); // precondition min = new_min; max = new_max; } #endif // INITIALIZER_OF_ARRAY_HPP_ 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 44 4 Reihungen und Abstraktionen Die C++-Initialisiererklasse unterscheidet sich in folgenden Implementationsaspekten von der Eiffel-Initialisiererklasse: Lesezugriff Inlining Redefinierbarkeit Konstruktor Aus den Abfragen min und max werden analog zum Java-Programm 4.18 geschützte Datenfelder min und max und öffentliche Funktionen get_min und get_max. Im Unterschied zu Java dürfen Felder und parameterlose Funktionen nicht gleich benannt sein. Die get-Funktionen sind innerhalb der Klassendefinition als Inline-Funktionen implementiert. Inline-Code wird an Aufrufstellen substituiert. Funktionen sollten normalerweise als virtual deklariert sein, denn nur so können sie polymorph verwendet, also redefiniert und dynamisch gebunden werden. In fast allen objektorientierten Sprachen seit Smalltalk ist polymorphe Benutzung der Standard (ausgenommen objektorientierte Erweiterungen älterer Sprachen). Die spezielle Markierung des Normalfalls mit virtual stammt aus Simula 67 und wurde so in C++ und leider auch in C# übernommen, aber zum Glück nicht in Java. Die print-Prozedur war in der Eiffel-Variante nicht nötig. Eine bessere C++-Variante davon würde wie in Programm 4.8 den <<-Operator überladen. Aus der make-Prozedur wird ein Konstruktor. Konstruktoren dienen der Initialisierung von Objekten, heißen immer wie ihre Klasse, haben keinen Typ und können überladen, aber nicht abstrakt und nicht virtuell sein. In C++ müssen Ableitungsklassen eigene Konstruktoren haben, während man in Eiffel geerbte Prozeduren zu Erzeugungsprozeduren erklären kann. Nun zu den Testerklassen. 4.4.2.2 Programm 4.28 C++: Generischer Testtreiber zu Reihungen vergleichbarer Elemente Testtreiber // Header file: TesterOfTSAC.hpp #ifndef TESTER_OF_TSAC_HPP_ #define TESTER_OF_TSAC_HPP_ #include <cassert> #include <iostream> #include <vector> #include "InitializerOfArray.hpp" template <typename T> class TesterOfTSAC // Instantiation condition: T is a comparable type. { protected: std::vector<T> * array; InitializerOfArray * ia; public: std::vector<T> & get_array () {return *array;} InitializerOfArray<T> & get_ia () {return *ia;} TesterOfTSAC (int size, T min, T max); }; // TesterOfTSAC © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 4.4 Abstrakte, generische und konkrete Testtreiber zu den Werkzeugkästen 4 – 45 template <typename T> TesterOfTSAC<T>::TesterOfTSAC (int size, T min, T max) { assert(size > 0); // precondition assert(min <= max); // precondition array = new std::vector<T>(size); ia = new InitializerOfArray<T>(min, max); ia->init_random(*array); std::cout << "array: "; ia->print(*array); } #endif // TESTER_OF_TSAC_HPP_ Erläuterungen zu ergänzen. Programm 4.29 C++: Testtreiber zu binärem Suchen in ganzzahligen Reihungen // Implementation file: TesterOfTSAIHas.cpp #include <cstdlib> #include <iostream> #include "ToolsetArrayComparableM.hpp" #include "TesterOfTSAC.hpp" namespace tsac = ToolsetArrayComparable; int main (int argc, char * argv[]) { if (argc < 4 || std::atoi(argv[1]) <= 0 || std::atoi(argv[2]) > std::atoi(argv[3])) { std::cout << "Please call this command with four integer arguments" << " size (> 0), min, max (>= min), item!"; } else { int size = std::atoi(argv[1]); int min = std::atoi(argv[2]); int max = std::atoi(argv[3]); int item = std::atoi(argv[4]); TesterOfTSAC<int> tester = TesterOfTSAC<int>(size, min, max); tsac::sort(tester.get_array()); std::cout << "item " << item; if (!tsac::has(tester.get_array(), item)) { std::cout << " not"; } std::cout << " found in array."; } std::cout << std::endl; return 0; } Erläuterungen zu ergänzen. Programm 4.30 C++: Testtreiber zu Indexbereich mit maximaler Summe in gleitpunktzahligen Reihungen // Implementation file: TesterOfTSADDetermine.cpp #include <cstdlib> #include <iostream> #include "ToolsetArrayComparableNumericM.hpp" #include "TesterOfTSAC.hpp" namespace tsac = ToolsetArrayComparable; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 4 – 46 4 Reihungen und Abstraktionen int main (int argc, char * argv[]) { if (argc < 3 || std::atoi(argv[1]) <= 0 || std::atof(argv[2]) > std::atof(argv[3])) { std::cout << "Please call this command with four integer arguments" << " size (> 0), min, max (>= min), item!"; } else { int size = std::atoi(argv[1]); double min = std::atof(argv[2]); double max = std::atof(argv[3]); TesterOfTSAC<double> tester = TesterOfTSAC<double>(size, min, max); int max_low, max_high; double max_sum; tsac::determine_range_with_max_sum (tester.get_array(), max_low, max_high, max_sum); std::cout << "max_low: " << max_low << " max_high: " << max_high << " max_sum: " << max_sum; } std::cout << std::endl; return 0; } Erläuterungen zu ergänzen. Da die Klasse InitializerOfArray für unsere Beispiele ausreicht, verzichten wir hier auf Ableitungsklassen. Aufgabe 4.15 C++: Abstrakte und konkrete Initialisierer und Testtreiber Schreiben Sie ausgehend von Programm 4.27 drei Initialisiererklassen in C++, die dem Entwurf Bild 4.3 und den Programmen 4.19, 4.20 und 4.21 entsprechen und erweitern Sie den generischen Testtreiber Programm 4.28 dem Entwurf Bild 4.4 entsprechend, aber nur soweit nötig, zu abstrakten und konkreten Testtreibern! Aufgabe 4.16 C++: Testtreiber zu Klassenvarianten der Werkzeugkästen Schreiben Sie C++-Varianten der Programme 4.29 und 4.30, die die Klassenvarianten der Werkzeugkästen und benutzen! 4.4.3 Java Aufgabe 4.17 Java: Testtreiber mit wiederverwendbaren Teilen Entwickeln Sie eine Java-Variante zu dieser Problemlösung! 4.4.4 C# Aufgabe 4.18 C#: Testtreiber mit wiederverwendbaren Teilen Entwickeln Sie eine C#-Variante zu dieser Problemlösung! 4.4.5 Fazit Zu ergänzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5 Programmeinheiten und -strukturen AufgabeBeispiel 5 Bild 5 5 (5) Leitlinie Programm 5 5 Tabelle 5 Die folgenden Kapitel untersuchen programmiersprachliche Konzepte und ihre Realisierungen in programmiersprachlichen Konstrukten. Die Vorgehensweise ist umgekehrt zu gängigen Erstsemester-Programmierkursen: Wir beginnen mit Konzepten, die für den Entwurf und die Spezifikation von Software im Großen wesentlich sind, und behandeln weitere softwaretechnisch relevante Konzepte vor speziellen unwesentlichen Details einzelner Sprachen. Wozu braucht man große Programmeinheiten? Dafür sprechen folgende softwaretechnische, miteinander verwobene Aspekte: Zerlegung, Modularisierung eines Systems, um seine Komplexität zu reduzieren programmiersprachliches Konstrukt, auf das sich große logische Einheiten aus Analyse und Entwurf abbilden lassen arbeitsteilige Entwicklung von Teilsystemen physische Einheit zum Speichern, Übersetzen, Binden, Laden von Programmteilen Kapselung von Programmteilen, Bildung von Namensräumen, um Namenskonflikte zu reduzieren Geheimnisprinzip, Datenabstraktion Trennung von Schnittstelle und Implementation Austauschbarkeit Wiederverwendbarkeit, Bibliotheken Erweiterbarkeit 5.1 Kompilationsarten und -einheiten Kompilation eines vollständigen Programms an einem Stück ist praktisch nicht brauchbar. Effiziente arbeitsteilige Entwicklung und Pflege sowie Austauschbarkeit und Wiederverwendbarkeit von Software erfordern die Möglichkeit, Programmteile einzeln zu kompilieren. Dazu gibt es verschiedene, historisch nacheinander entwickelte Arten: Unabhängige Kompilation Getrennte Kompilation Bei unabhängiger Kompilation wird ein Programmteil ohne Kenntnis der anderen Teile des Gesamtprogramms kompiliert. Erst beim Binden der einzelnen Teile sind offene Referenzen feststellbar. Typfehler werden u.U. nicht einmal vom Laufzeitsystem erkannt. Dieser Mechanismus ist für Kompilierer und Binder einfach, aber für Programmierende fehleranfällig. Getrennte Kompilation bedeutet, dass zur Kompilationszeit alle verwendeten Größen bekannt sind und Typprüfungen über die Grenzen des kompilierten Teils hinaus durchgeführt werden – so, als ob das Programm als Ganzes kompiliert wird. Dazu müssen die Kompilationseinheiten genau definierte Schnittstellen besitzen. Der Kompilierer überprüft, ob eine Kundeneinheit sich an die Schnittstelle einer benutzten Lieferanteneinheit hält (z.B. zeitliche Reihenfolge der Kompilation, Parameteranzahl, Typverträglichkeit). Dieser Mechanismus ist für Kompilierer und Binder aufwändiger, bietet aber mehr Sicherheit gegen Programmierfehler. Oft ist von getrennter Kompilation die Rede, wo tatsächlich nur unabhängige Kompilation vorliegt. Speicherungs- und Kompilationseinheit Die Speicherungseinheit für Programmteile ist meist eine Datei, wie sie das Dateisystem des Betriebssystems bereitstellt. Zu unterscheiden sind Quellprogrammdateien © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 5 – Seite 1 von 18 5–2 5 Programmeinheiten und -strukturen (source file) und Objektcodedateien (object file). Zu ihrer Unterscheidung kann ein Kompilierer bestimmte Dateinamenserweiterungen und Verzeichnisstrukturen fordern. Eine Kompilationseinheit ist eine syntaktische Einheit der Sprache, die der Kompilierer übersetzen kann. Oft entspricht einer Kompilationseinheit das Startsymbol der Syntaxregeln. Zwischen Kompilations- und Speicherungseinheiten sind u.A. folgende Beziehungen möglich: (1) k:q:o: Beim orthogonalen Ansatz können k Kompilationseinheiten auf q Quelldateien verteilt sein, aus denen o Objektcodedateien erzeugt werden. k:1:k: Eine Quelldatei kann k Kompilationseinheiten enthalten, aus denen k Objektcodedateien erzeugt werden. k:1:1: Eine Quelldatei kann k Kompilationseinheiten enthalten, aus denen eine Objektcodedatei erzeugt wird. 1:q:1: Eine Kompilationseinheit kann auf q Quelldateien verteilt sein, aus denen eine Objektcodedatei erzeugt wird. 1:1:o: Eine Quelldatei enthält eine Kompilationseinheit, aus denen o Objektcodedateien erzeugt werden. 1:1:1: Jede Kompilationseinheit ist in genau einer Quelldatei enthalten, die sonst nichts enthält und aus der genau eine Objektcodedatei erzeugt wird. (2) (3) (4) (5) (6) (1) ist die flexibelste, aber auch komplexeste Option, während (6) die unflexibelste, aber einfachste Option darstellt. Bei (6) können eine Kompilationseinheit und die zugehörigen Dateien fast identische, eindeutig aufeinander abbildbare Namen haben, was die Organistation und Handhabbarkeit vereinfacht. Bei den anderen Optionen sind Namen von Kompilationseinheiten und Dateien zu unterscheiden. Vor- und Nachteile der Optionen sind gegeneinander abzuwägen, um zu entscheiden, ob sich der Mehraufwand für eine der Optionen (1) bis (5) lohnt. Entwurfsaspekte Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen: Soll der Kompilierer unabhängig oder getrennt kompilieren? Was ist eine Kompilationseinheit? Wie sind Kompilations- und Speicherungseinheiten einander zugeordnet? Tabelle 5.1 Physische Einheiten Eigenschaft Component Pascal Kompilationseinheit zu Dateien Eiffel C++ 1:1:1 Kompilationseinheit Klasse Java 1:q:1 1:1:o fast beliebige Programmteile mehrere Klassen und Schnittstellen Modul Ladeeinheit Kompilationsart 5.1.1 System getrennt C# unabhängig Klasse System oder einzelne Klassen und Methoden einer Ansammlung getrennt Component Pascal Module sind die einzigen Kompilationseinheiten, sie werden getrennt am Stück in Objektcode kompiliert. Component Pascal ist so entworfen, dass trotz dynamischen Ladens von Modulen keine Schnittstellenverträglichkeitsfehler zur Laufzeit vorkom- © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.1 Kompilationsarten und -einheiten 5–3 men. Zu jedem Modul gibt es i.d.R. eine Quelltextdatei. Der Kompilierer erzeugt aus dem Quelltext eine Schnittstellen- und eine Objektcodedatei. Durch die IMPORTAbschnitte kennt der Kompilierer die benutzten Module. Per Namenskonvention findet er ihre Schnittstellendateien und kann so die Einhaltung der Schnittstellen zwischen den Modulen prüfen. Die IMPORT-Beziehung ist zyklenfrei. ☺ Da der Kompilierer die Schnittstellendateien automatisch erzeugt, ist die Konsis- tenz zwischen Objektcode und Schnittstelle stets garantiert. ☺ Benutzte Schnittstellen liegen kompiliert vor. Das verkürzt Kompilationszeiten. ☺ IMPORT-Ketten bilden kein Problem, da der Kompilierer sie auflöst. ☺ Wird die Schnittstelle eines Moduls erweitert oder seine Implementation geändert, so brauchen die Kunden dieses Moduls nicht nachkompiliert werden. Nachkompilieren ist erst erforderlich, wenn Schnittstellen benutzter Dienste geändert wurden. BlackBox enthält kein Werkzeug für automatisches Nachkompilieren. 5.1.2 Eiffel In Eiffel sind Klassen die einzigen Kompilationseinheiten, sie werden getrennt am Stück in Objektcode kompiliert. Zu jeder Klasse gibt es eine gleichnamige Datei mit der Erweiterung „.e“, die genau ihren Quelltext enthält. Durch die typisierten Vereinbarungen in der Klasse kennt der Kompilierer die benutzten Klassen. Per Namenskonvention findet er ihre Dateien und kann dadurch die Einhaltung der Schnittstellen (Typverträglichkeit) prüfen. ☺ Eiffel-Entwicklungsumgebungen sorgen meist für automatisches Nachübersetzen und damit dafür, dass die übersetzten Einheiten konsistent zusammenpassen. ☺ Benutzungsketten bilden kein Problem, da der Kompilierer sie auflöst. Erweiterungen von Schnittstellen und Änderungen an Implementationen führen zu Nachkompilierungen von Kunden, da der Kompilierer nur erkennen kann, dass der Inhalt einer Lieferantendatei geändert wurde, aber nicht die Art der Änderung. Manche Sprachmerkmale wie kovariante Vererbung erfordern eine globale Analyse der Quelltexte. Eiffel würde daher dynamisches Laden von Komponenten nur auf Kosten von Sicherheit oder mehr Laufzeitprüfungen erlauben. Globale Analyse verlängert Kompilationszeiten. Verzichtet der Kompilierer auf globale Analyse, so sind Typfehler zur Laufzeit möglich. 5.1.3 C++ In C++ sind Dateien ziemlich beliebigen Inhalts (die einzigen) Kompilationseinheiten, sie werden unabhängig am Stück in Objektcode kompiliert. Danach bindet der Binder die einzelnen Codestücke zusammen, dabei löst er externe Referenzen auf. Er stellt doppelt definierte Namen und offene Referenzen fest, kann aber keine Parameteranzahl- und -typprüfungen übernehmen, da ihm die dazu erforderlichen Informationen fehlen. Die unabhängige Kompilation in C++ folgt aus dem Entwurfsziel der Kompatibilität mit C und der Zusammenarbeit mit der relativ primitiven Umgebung des C-Binders. Als Konsequenz kann der Kompilierer nicht garantieren, dass Größen eindeutig definiert, Informationen (z.B. über Typen) konsistent oder Größen initialisiert sind, bevor sie benutzt werden. Typsicherheit ist nicht garantiert. C++ bietet wie C mit Vorübersetzerdirektiven Mittel, die – wenn sie diszipliniert benutzt werden – näherungsweise getrennte Kompilation ermöglichen, um der Fehleranfälligkeit unabhängiger Kompilation entgegenzuwirken. Mit Inkludierung sind nämlich mehrere Dateien gemeinsam kompilierbar. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5–4 5 Programmeinheiten und -strukturen Im einfachsten Fall schreibt der Programmierer zu jeder Kompilationseinheit (Implementationsdatei, source file, Namenskonvention: Dateiname.c oder .cpp) eine Schnittstellendatei (header file, Namenskonvention: Dateiname.hpp). Eine Kundendatei inkludiert die Schnittstellendatei eines Lieferanten. Durch den Effekt der #include-Direktive kennt der Kompilierer den Inhalt der Schnittstellendatei und kann dadurch die Einhaltung der Schnittstelle zwischen Kunden- und Lieferantendatei überprüfen. Die Inkludierungsbeziehung muss zyklenfrei sein. Der Programmierer ist für die Konsistenz zwischen Implementations- und Schnittstellendatei verantwortlich. Häufige Fehlerquelle bleibt die Inkludierung falscher oder nicht aktualisierter Schnittstellendateien. Bei jeder Inkludierung wird eine Schnittstellendatei neu kompiliert. Das verlängert Kompilationszeiten. Wird durch Inkludierungsketten dieselbe Schnittstellendatei mehrfach inkludiert, so findet der Kompilierer mehrfach vorkommende Namen, produziert also Kontextfehlermeldungen. Programmierkonventionen mit Vorübersetzerdirektiven (#ifndef, #define, #endif) lösen dieses Problem. Inhalt der Schnittstellendatei xmp.hpp: Beispiel #ifndef XMP_HPP_ #define XMP_HPP_ #include "other.hpp" Definitionen von Konstanten und Typen Externdeklarationen exportierter Funktionen #endif // XMP_HPP_ Die Schnittstellendatei ist keine Schnittstellendatei im strikten Wortsinn, da sie nicht nur Schnittstelleninformationen enthält, sondern auch Implementationsteile. Im Falle generischer Einheiten enthält sie die vollständige Implementation. Werkzeuge wie make, die Nachkompilationen gemäß Abhängigkeiten zwischen Kompilationseinheiten steuern, entscheiden oft aufgrund der Zeitstempel der Dateien. Das führt bei Schnittstellenerweiterungen und Implementationsänderungen zu unnötigen Nachkompilationen von Kunden, die diese Erweiterungen bzw. Änderungen gar nicht betreffen. 5.1.4 Java In Java sind Kompilationseinheiten Ansammlungen von Klassen und Schnittstellen, sie werden getrennt am Stück in Bytecode kompiliert. Zu jeder Kompilationseinheit gibt es eine Quelldatei, die denselben Namen wie die erste Klasse der Einheit trägt mit der Erweiterung „.java“. Zu jeder Klasse gibt es eine gleichnamige Bytecodedatei mit der Erweiterung „.class“. Durch die typisierten Vereinbarungen in der Kompilationseinheit kennt der Kompilierer die benutzten Klassen. Per Namenskonvention findet er ihre Dateien und kann dadurch die Einhaltung der Schnittstellen prüfen. Aufgabe 5.1 Java: Getrennte Kompilation Finden Sie heraus, welche Eigenschaften getrennte Kompilation bei Java hat! 5.1.5 C# Zu ergänzen. Aufgabe 5.2 C#: Getrennte Kompilation Finden Sie heraus, welche Eigenschaften getrennte Kompilation bei C# hat! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.2 Zusammenfassung und Kapselung von Programmteilen 5–5 5.2 Zusammenfassung und Kapselung von Programmteilen Kapselung Kapselung (encapsulation) bedeutet, Daten, Operationen und andere Programmteile zu einer Einheit – einer Kapsel – zusammenzufassen, die i.d.R. ein Sprachkonstrukt mit einem Namen ist. Gekapselte Teile sind i.d.R. nur mit qualifizierten Namen zugreifbar. Kapselung wird oft zusammen mit Geheimnisprinzip (information hiding) und Datenabstraktion (data abstraction) genannt und manchmal damit verwechselt. Tatsächlich folgt jedoch aus dem Kapseln noch kein Verstecken von Programmteilen. Kapselung kleiner Programmteile Bei kleinen Programmteilen unterscheiden wir die Kapselung von Algorithmen und die Kapselung von Daten: Algorithmenkapselung ist lang bekannt: Routinen (Prozeduren, Funktionen) z.B. in Fortran 1957, Koroutinen z.B. in Simula 67, Prozesse in Concurrent Pascal 1974. Datenkapselung ist in Form von Verbunden (record) in Cobol seit 1960 bekannt. Tabelle 5.2 Kapselung kleiner und großer Programmteile Eigenschaft Component Pascal Eiffel Algorithmenkapselung Prozedur Routine Verbund Datenkapselung Modul - C++ Java C# Funktion Verbund Datei - ADT Paket Namensraum Klasse Modul Typenkapselung Kapselung großer Programmteile Entwurfsaspekte sprachextern: Subsystem sprachextern: Cluster Namensraum sprachextern: Ansammlung Auf die kombinierte Kapselung von Daten und Algorithmen mit Modulen und Klassen gehen wir in 5.4 und 6 ein. Die softwaretechnisch interessante Frage ist hier, ob und welche Sprachkonstrukte es gibt, mit denen man große Programmteile, also etwa viele Module oder Klassen, zu größeren Einheiten zusammenfassen kann. Gründe dafür sind: Kapselung hilft, bei großen Systemen große Analyse- und Entwurfseinheiten abzubilden. Ohne Kapselungsmöglichkeit treten unvermeidlich Namenskonflikte auf, wenn unabhängig voneinander entwickelte Teilsysteme zu einem System integriert werden sollen. Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen: Soll das Problem außerhalb der Sprache durch Zusammenfassungen von Programmteilen oder innerhalb der Sprache durch ein Kapselungskonstrukt behandelt werden? Soll dafür ein schon vorhandenes Kapselungskonstrukt verwendet oder ein zusätzliches eingeführt werden? Soll das Kapselungskonstrukt nur Sichtbarkeitsbereiche oder auch Zugriffsrechte definieren? 5.2.1 Component Pascal Component Pascal bietet keine größeren Kapseln als Module. Ein System kann leicht hunderte Module umfassen. Vereinigt man zwei Systeme, so sind Konflikte bei Modul- 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5–6 5 Programmeinheiten und -strukturen namen nicht ausgeschlossen. Component Pascal delegiert die Lösung dieses Problems an seine Umgebung. BlackBox arbeitet mit Subsystemen. Ein Subsystem ist eine Menge von Modulen. Subsysteme zerlegen ein System in disjunkte Teilmengen von Modulen. Jedes Subsystem hat einen Namen, der bei Oberon microsystems registriert werden kann und dann weltweit eindeutig ist. Vereinigt man zwei in Subsysteme zerlegte Systeme, so sind Namenskonflikte bei registrierten Subsystemen ausgeschlossen, bei nicht registrierten Subsystemen unwahrscheinlich. Die Zuordnung von Modulen zu Subsystemen erfolgt durch eine einfache Namenskonvention. Der Subsystemname ist Teil des Modulnamens. Ein Modul einem anderen Subsystem zuordnen erfordert eine Änderung des Modulnamens und damit des Quelltexts des Moduls. Jedem Subsystem ist ein Dateiverzeichnis zugeordnet, das alle Module des Subsystems enthält. Da Subsysteme kein Teil der Sprache sind, können sie keine eigenen Sichtbarkeitsbereiche oder Zugriffsrechte definieren. 5.2.2 Eiffel Eiffel bietet keine größeren Kapseln als Klassen. Ein System kann leicht viele hunderte Klassen umfassen. Vereinigt man zwei Systeme, so sind Konflikte bei Klassennamen wahrscheinlich. Eiffel delegiert die Lösung dieses Problems an seine Umgebung. ISE Eiffel arbeitet mit Clustern. Ein Cluster ist eine Menge von typischerweise 5 bis 40 Klassen. Cluster zerlegen ein System in disjunkte Teilmengen von Klassen. Jedem Cluster ist ein Dateiverzeichnis zugeordnet, das alle Klassen des Clusters enthält. Namenskonflikte sind trotz der Cluster nicht ausgeschlossen; sie werden durch lokales Umbenennen konfligierender Klassen in einer Ace-Datei gelöst, der Quelltext der Klassen bleibt davon unberührt. Ace steht für Assembly of Classes in Eiffel. Eine Ace-Datei wird in Lace (Language for the Ace) geschrieben und steuert die Kompilation eines Systems. Beispiel 5.1 Eiffel: Ace-Datei1 system example root CALCULATOR (my_cluster1): "make" default assertion (ensure); precompiled ("$EIFFEL3|precomp|spec|$PLATFORM|base"); debug (no) cluster my_cluster1: "mydir|project1|subdir"; her_cluster2: "herdir|project2|subdir1|subdir2" adapt my_cluster1: rename STACK as MY_STACK; end Da Cluster kein Teil der Sprache sind, können sie keine eigenen Sichtbarkeitsbereiche oder Zugriffsrechte definieren. 5.2.3 C++ C++ bietet mit Namensräumen größere Kapseln als Klassen und Dateien. Ein Namensraum (namespace) umfasst eine Menge von Vereinbarungen. 1 http://burks.brighton.ac.uk/burks/language/eiffel/oveiffel/system.htm (Zugriff 2004-10-01). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.2 Zusammenfassung und Kapselung von Programmteilen 5–7 namespace Xmp { Vereinbarungen } // end of Xmp Namensräume wurden in C++ eingeführt, um das C-Problem des globalen Namensraums zu lösen, haben einen Namen, der kundenseitig Aliasnamen erhalten kann, definieren einen Sichtbarkeitsbereich durch einen zugehörigen Block, aber keinen Schutz und keine Zugriffsrechte, liegen orthogonal zu Dateien, d.h. ein Namensraum kann sich über mehrere Dateien erstrecken und eine Datei kann Teile mehrerer Namensräume enthalten, können textuell geschachtelt werden, erlauben es z.B., Module und Subsysteme von Component Pascal nachzubilden (siehe 5.4). Programm 5.1 C++: using-Direktive #include <iostream> using namespace std; int main (int argc, char * argv[]) { cout << "Hello World" << endl; return 0; } Beseitigter Namensraum Bei dieser leider oft im Web zu findenden Hello-World-Variante sorgt die using-Direktive dafür, dass die Namen cout und endl aus dem Standardnamensraum std unqualifiziert verwendbar sind; die qualifizierten Namen lauten bekanntlich std::cout und std::endl. „It is generally in poor taste to dump every name from a namespace into the global namespace“ [Str97] S. 47. Stroustrup hat using als Migrationsmittel vorgesehen, das ermöglicht, Programmteile einzubinden, die vor der Einführung von Namensräumen erstellt wurden [Str97] S. 172, 183. Freilich sind so Missbräuche von using nicht zu verhindern; Programmierrichtlinien können helfen. Was ist problematisch an using? Namensräume und qualifizierte Zugriffe sollen die in einem globalen Namensraum auftretenden Namenskonflikte vermeiden helfen, die Lesbarkeit einzelner Kompilationseinheiten erhöhen, da ein qualifizierter Name den Ort seiner Definition anzeigt. Das Entqualifizieren von Namen widerspricht diesen softwaretechnischen Zielen. Entqualifizierte Namen können zu Überladungen führen, die unbeabsichtigte Effekte bewirken. Solche Fehler sind schwer zu lokalisieren, da alle gleichen Namen in allen mit using benutzten Namensräumen involviert sein können. Leitlinie 5.1 C++: Vermeide using 27.9.12 Vermeide, using-Direktiven zu verwenden, da sie die Lesbarkeit mindern und zu Namenskonflikten und unbeabsichtigten Überladungen führen können! Verwende using nur, um alte Programmteile unverändert zu benutzen! Verwende namespaceAlias-Definitionen statt using! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5–8 Programm 5.2 C++: Namensraum mit Aliasnamen 5.2.4 5 Programmeinheiten und -strukturen #include <iostream> namespace s = std; int main (int argc, char * argv[]) { s::cout << "Hello World" << s::endl; return 0; } Java Java bietet mit Paketen größere Kapseln als Klassen. Ein Paket (package) besteht aus mehreren Kompilationseinheiten und enthält Unterpakete, Klassen und Schnittstellen. package Xmp; Importvereinbarungen Typenvereinbarungen Pakete sind Teil der Sprache, haben einen Namen, definieren einen Sichtbarkeitsbereich durch eine Paketvereinbarung, die für die Kompilationseinheit gilt, und Zugriffsrechte, können sich über mehrere Kompilationseinheiten erstrecken, können logisch geschachtelt werden. Java-Pakete unterscheiden sich von C++-Namensräumen wesentlich darin, dass sie auch Schutzräume darstellen. Das Analogon zur C++-using-Direktive ist die Java-import-Deklaration. Das softwaretechnisch fragwürdige import kann kaum als Migrationsmittel wie bei C++ konzipiert sein. Leitlinie 5.2 Java: Vermeide import 5.2.5 Vermeide, import-Deklarationen zu verwenden, da sie die Lesbarkeit mindern und zu Namenskonflikten und unbeabsichtigten Überladungen führen können! C# C# bietet mit Namensräumen ähnlich wie C++ größere Kapseln als Klassen. Ein Namensraum umfasst eine Menge von Klassen. namespace Xmp { using-Direktiven Typenvereinbarungen } // end of Xmp Namensräume sind Teil der Sprache, haben einen Namen, definieren einen Sichtbarkeitsbereich, liegen orthogonal zu Dateien, können textuell oder logisch geschachtelt werden. Weiter bietet C# Ansammlungen. Eine Ansammlung (assembly) ist eine Menge von Dateien, die zusammen eine DLL (dynamic link library) oder ein EXE (executable) ergeben. Eine Ansammlung hat höchstens einen Eintrittspunkt. Der Modifikator internal zeigt an, was nur in der gegebenen Ansammlung sichtbar sein soll. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.3 Geheimnisprinzip, Schutz und Zugriffsrechte Programm 5.3 C#: using-Direktive 5–9 using System; public class HelloWorld { public static void Main (string[] args) { Console.WriteLine("Hello World"); } } In Programm 5.3 muss der Name Console wegen der using-Direktive nicht voll qualifiziert werden. Das C#-Analogon zur C++-using-Direktive und Java-import-Deklaration ist softwaretechnisch genauso fragwürdig wie jene. Leitlinie 5.3 C#: Vermeide using 5.3 Vermeide, using-Direktiven zu verwenden, da sie die Lesbarkeit mindern und zu Namenskonflikten und unbeabsichtigten Überladungen führen können! Geheimnisprinzip, Schutz und Zugriffsrechte Tabelle 5.3 Schutz, Rechte und Verbote Eigenschaft Schutzeinheit Component Pascal Eiffel C++ Java C# Modul Objekt Datei Klasse Klasse Paket Klasse Ansammlung Zugriffsrechte außerhalb der Schutzeinheit spezifizierbar? Datenelement lesen-schreiben ja nein Datenelement nur lesen ja ja nein Routine ausführen ja Routine nur redefinieren ja nein Zugriffsverbote außerhalb der Schutzeinheit spezifizierbar? Objekt vereinbaren ja Objekt erzeugen Objekt zuweisen ja ? Weitere Rechte und Verbote gibt es im Zusammenhang mit Vererbung. 5.3.1 Component Pascal MODULE Xmp; TYPE XmpClass* = RECORD xmpReadWrite : INTEGER; globalReadOnly- : REAL; globalReadWrite* : CHAR; END; VAR xmpReadWrite globalReadOnlyglobalReadWrite* 27.9.12 : INTEGER; : REAL; : CHAR; (* Meist schlechter Stil! *) (* Meist schlechter Stil! *) © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5 – 10 5 Programmeinheiten und -strukturen PROCEDURE XmpExec; BEGIN Anweisungen END XmpExec; PROCEDURE GlobalExec*; VAR loc : INTEGER; BEGIN Anweisungen END GlobalExec; PROCEDURE (VAR this : XmpClass) GlobalRedefineOnly-; BEGIN Anweisungen END GlobalRedefineOnly; END Xmp. 5.3.2 Eiffel class XMP_CLASS feature {NONE} xmp_object_read_write : CHAR feature {XMP_CLASS} xmp_class_read_only : INTEGER feature {OTHER_CLASS} other_class_read_only : BOOLEAN feature {ANY} global_read_only : REAL -- ANY ist Standard feature global_exec local loc : INTEGER do Anweisungen end -- global_exec end -- class XMP_CLASS 5.3.3 C++ class Xmp_class { private: int xmp_class_read_write; // private ist hier Standard protected: int xmp_derived_class_read_write; public: char global_read_write; // Meist schlechter Stil! void global_exec () { int loc; Anweisungen } // end of global_exec }; // end of Xmp_class static int file_read_write; char global_read_write; // Oft schlechter Stil! © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.3 Geheimnisprinzip, Schutz und Zugriffsrechte 5 – 11 static int file_exec () { int loc; Anweisungen }; // end of file_exec void global_exec () { int loc; Anweisungen }; // end of global_exec 5.3.4 Java package Xmp; class PrivClass { private int privClassReadWrite; protected int xmpExtendedPrivClassReadWrite; public char xmpReadWrite; // Meist schlechter Stil! public void xmpExec () { int loc; Anweisungen } // end of xmpExec bool xmpReadWrite; }; // end of XmpPrivateClass public class PubClass { private int pubClassReadWrite; protected int extendedPubClassReadWrite; public char globalReadWrite; // Meist schlechter Stil! public void globalExec () { int loc; Anweisungen } // end of globalExec boolean xmpReadWrite; }; // end of XmpPubClass 5.3.5 C# namespace Xmp { class PrivClass { private int privClassReadWrite; protected int xmpExtendedPrivClassReadWrite; public char xmpReadWrite; // Meist schlechter Stil! public void xmpExec () { int loc; Anweisungen } // end of xmpExec bool xmpReadWrite; }; // end of XmpPrivClass 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5 – 12 5 Programmeinheiten und -strukturen public class PubClass { private int pubClassReadWrite; protected int extendedPubClassReadWrite; public char globalReadWrite; // Meist schlechter Stil! public void globalExec () { int loc; Anweisungen } // end of globalExec bool xmpReadWrite; }; // end of XmpPubClass }; // end of Xmp © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Component Pascal Syntax Eiffel Exportmarke an jedem Merkmal C++ Abschnitt für mehrere Merkmale Java C# Modifikator bei jedem Merkmal Semantik Zugriffsberechtigte Zugriffsrechte alle Klassen/ alles in der Umgebung alle Klassen im selben Paket/Programm und alle Erweiterungsklassen „*“ ☺ Datenelement nur lesen „-“ feature Routine ausführen „*“ feature {ANY} nur-lesen, ausführen public feature {CLIENT1, CLIENT2,...} ☺ protected protected internal kein Modifikator internal protected, jede Klasse in eigenem Paket protected alle alles im selben Modul/ Paket/Programm nur Erweiterungsklassen public keine Exportmarke Routine nur redefinieren „-“ feature {NONE} lesen-schreiben, ausführen nur Klasse selbst Legende: Ein ☺, oder feature {} keine Exportmarke, Klasse allein in eigenem Modul ☺ protected private oder nichts private bedeutet „nicht möglich“. 5 – 13 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 durch Liste spezifizierte Kundenklassen Datenelement lesen-schreiben 5.3 Geheimnisprinzip, Schutz und Zugriffsrechte 27.9.12 Tabelle 5.4 Schutz und Zugriffsrechte bei Klassen 5 – 14 5.4 5 Programmeinheiten und -strukturen Module und Datenabstraktion Ein Modul ist eine Programmeinheit, die Daten und Routinen zu einer bezeichneten Einheit zusammenfasst (kapselt) und eine Schnittstelle für externe Zugriffe festlegt. Module sind Bausteine modularer Programme. Datenabstraktion bedeutet das Verbergen der internen Struktur gekapselter Daten hinter einer für externe Zugriffe bereitgestellten Schnittstelle. Eine abstrakte Datenstruktur (ADS) ist ausschließlich durch ihr von außen beobachtbares Verhalten definiert. Sie ist spezifiziert durch eine Menge anwendbarer Operationen, wobei jede Operation durch ihre Signatur, ihre Aufrufkonvention und ihren Effekt beschrieben ist. Module sind oft ADSen. Leitlinie 5.4 Module als ADSen Entwerfe Module als abstrakte Datenstrukturen! Verbiete externe Schreibzugriffe auf Daten von Modulen! Ein abstrakter Datentyp (ADT) kombiniert Eigenschaften von ADSen und von Typen. Von einem ADT können beliebig viele Exemplare existieren, die alle ADSen sind. Eine Klasse ist ein erweiterbarer ADT, dessen Exemplare Objekte heißen. Da alle fünf Sprachen ein Klassenkonstrukt bieten, lässt sich in jeder ein Modul auch mit einer Klasse realisieren. Nach dem bekannten Singleton-Entwurfsmuster ist eine Klasse zu definieren und ein einzelnes Objekt dieser Klasse zu vereinbaren und ggf. zu erzeugen, das als gewünschtes Modul fungiert. Dies ist allerdings relativ aufwändig und erfordert zusätzliche Maßnahmen, um das Modul-Objekt an gewünschten Stellen sichtbar zu machen und zu verhindern, dass weitere Objekte der Klasse erzeugt werden. Entwurfsaspekte Die folgenden Entwurfsaspekte erlauben verschiedene Entwurfsalternativen: Soll es ein eigenes Sprachkonstrukt für Module geben? Falls ja, wie verhalten sich Module und Klassen zueinander? Falls nein, soll das Klassenkonstrukt Module als Singleton-Objekte oder anders unterstützen? Im Folgenden betrachten wir ein abstraktes Beispiel mit zwei Modulen in einer Kunden-Lieferanten-Beziehung. Wir nutzen es als Vorlage, um mögliche Realisierungen von Modulen in den verschiedenen Sprachen zu demonstrieren. Beispiel 5.2 Modulare Benutzungsbeziehung 5.4.1 Client Supplier Component Pascal Component Pascal hat ein Sprachkonstrukt MODULE. Programm 5.4 Component Pascal: Kunden-LieferantenModule MODULE I3Supplier; TYPE T* = RECORD ... END; PROCEDURE Do* (IN t : T); BEGIN ... END Do; END I3Supplier. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.4 Module und Datenabstraktion 5 – 15 MODULE I3Client; IMPORT S := I3Supplier; PROCEDURE Do* (IN t : S.T); BEGIN S.Do (t) END Do; END I3Client. Abstrakte Datenstrukturen ermöglicht Component Pascal durch das Modulkonstrukt. Es ist darauf zu achten, dass keine Variablen exportiert werden (höchstens nur lesbar). 5.4.2 Eiffel Eiffel hat kein Sprachkonstrukt für Module. Unterstützung für abstrakte Datenstrukturen gibt es nur in Form von Exemplaren von Klassen, siehe nächsten Abschnitt. Zum Sichtbarmachen von Modul-Objekten benutzt man Einmalfunktionen und mehrfaches Erben (Mix-Ins). 5.4.3 C++ C++ hat kein Sprachkonstrukt für Module oder abstrakte Datenstrukturen. Sie sind neben der oben genannten Möglichkeit von Singleton-Objekten auf zwei Arten mit drei Sprachelementen realisierbar. 5.4.3.1 Prämodule Das erste Sprachelement stammt von C und realisiert Prämodule mit Schnittstellenund Implementationsdateien. Ein Prämodul fasst Daten und Operationen zu einer Einheit zusammen und legt eine Schnittstelle für externe Zugriffe fest, hat aber keinen Namen. Die in einem Prämodul vereinbarten Namen sind global, nicht gekapselt. Prämodule lassen sich in C/C++ durch zwei (oder mehr) Dateien realisieren: Schnittstellendatei Implementationsdatei Eine Schnittstellendatei (header file) enthält Definitionen von Konstanten, Definitionen von Typen, Externdeklarationen exportierter Variablen (schlechter Programmierstil), Externdeklarationen exportierter Funktionen. Eine Implementationsdatei (source file) enthält Definitionen nicht exportierter Konstanten, Typen und Funktionen, Definitionen nicht exportierter Variablen, Definitionen exportierter Variablen (schlechter Programmierstil), Definitionen exportierter Funktionen. Man beachte, dass eine Schnittstellendatei keine Definitionen enthalten darf, die Speicherplatz beanspruchen! Nicht die Sprache erzwingt eine disziplinierte Vorgehensweise, sondern der Programmierer ist selbst dafür verantwortlich. 5.4.3.2 Namensräume Das zweite Sprachelement sind die Namensräume von C++. Sie kapseln zwar Namen, bilden aber keine Schutzeinheiten, d.h. eignen sich nicht dazu, Schnittstellen festzulegen. Kombiniert man jedoch die Möglichkeiten von Dateien und Namensräumen, so lassen sich Module realisieren. Programm 5.5 bietet etwas mehr als Programm 5.4, da I3 hier als Namensraum einen eigenen Sichtbarkeitsbereich definiert, während I3 in Programm 5.4 nur ein Subsystem ist. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5 – 16 Programm 5.5 C++: KundenLieferanten-Module 5 Programmeinheiten und -strukturen Inhalt der Schnittstellendatei C++/I3/Include/supplier.hpp: // Header file: supplier.hpp: #ifndef I3_SUPPLIER_HPP_ #define I3_SUPPLIER_HPP_ namespace I3 { namespace Supplier { class T {...}; extern void do (const T & t); } } #endif // I3_SUPPLIER_HPP_ Inhalt der Implementationsdatei C++/I3/Source/supplier.cpp: // Implementation file: supplier.cpp: #include "supplier.hpp" namespace I3 { namespace Supplier { void do (const T & t) { ... } } } Inhalt der Schnittstellendatei C++/I3/Include/client.hpp: // Header file: client.hpp: #include "supplier.hpp" #ifndef I3_CLIENT_HPP_ #define I3_CLIENT_HPP_ namespace I3 { namespace Client { namespace S = I3::Supplier; extern void do (const S.T & t); } } #endif // I3_CLIENT_HPP_ Inhalt der Implementationsdatei C++/I3/Source/client.cpp: // Implementation file: client.cpp: #include "supplier.hpp" #include "client.hpp" namespace I3 { namespace Client { namespace S = I3::Supplier; void do (const S.T & t) { S.do(t); } } } © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 5.4 Module und Datenabstraktion 5.4.3.3 5 – 17 Klassenmethoden Eine andere Realisierung von Modulen in C++, die „programming more enjoyable for the serious programmer“ macht, sind Klassen mit static-Merkmalen, die nicht nur bei ihrer Definition, sondern auch bei Aufrufen an die Klasse gebunden sind, siehe 6.4 S. 6-7. 5.4.4 Java, C# Java und C# übernehmen von C++ das Sprachkonstrukt der static-Merkmale und die Möglichkeit, damit Module zu realisieren, siehe 6.4 S. 6-7. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5 – 18 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 5 Programmeinheiten und -strukturen 27.9.12 6 Klassen und objektorientierte Konzepte Aufgabe Beispiel 6 Bild 6 waren 6 (6) unsere Leitlinie Programm 6 6 Tabelle 6 Traditionell Programmiersprachen dominiert von den Maschinen, auf denen sie ausführbar sein sollten. Doch in dem Maße, in dem unsere Fähigkeit wuchs, Sprachkonstrukte in Maschinenkonzepte zu übersetzen, nahm auch unsere Freiheit zu, die Sprachen an die Welten der Probleme anzupassen. So ist es heute ein zentrales Anliegen bei der Entwicklung moderner Programmiersprachen, Situationen, wie sie in Anwendungen typisch auftreten, unmittelbar in Sprachkonzepten widerzuspiegeln. Peter Pepper1 6.1 Abstrakte Datentypen Ein abstrakter Datentyp (ADT) kombiniert die Typeigenschaft mit der Eigenschaft der ADS (s. 5.4 S. 5-14): Von einem Typ können beliebig viele Exemplare vereinbart oder erzeugt werden. Der Typ ist ein Bauplan für seine Exemplare. Jedes Exemplar eines ADTs ist eine ADS mit eigenem Zustandsraum. Ein ADT legt eine Schnittstelle und damit ein Verhalten fest. Alle Exemplare eines ADTs übernehmen die Operationen ihres Typs, aber jedes Exemplar hat seine eigenen Daten, auf denen die Operationen arbeiten. Component Pascal ADTen sind durch das Verbundkonstrukt RECORD ermöglicht. Es ist darauf zu achten, dass keine Felder exportiert werden (höchstens nur lesbar). Eiffel ADTen sind durch das Klassenkonstrukt class gut unterstützt: Eine Klasse ist ein (ggf. nur partiell) implementierter abstrakter Datentyp. C++, C# ADTen sind durch das Klassenkonstrukt class und das Verbundkonstrukt struct ermöglicht. Es ist darauf zu achten, dass keine Datenelemente öffentlich gemacht werden. Java ADTen sind durch das Klassenkonstrukt class ermöglicht. Es ist darauf zu achten, dass keine Datenelemente öffentlich gemacht werden. 6.2 Klassen Klassen sind Grundelemente objektorientierter Programme. Eine Klasse ist ein ADT, der beerbbar und partiell implementiert ist. Wie ADTen vereinen Klassen Merkmale von ADSen und Typen. Eine Klasse fasst Daten und Operationen zu einer bezeichneten Einheit zusammen und legt Schnittstellen zu Kunden- und Unterklassen fest. Die Schnittstellen sollen rein operational sein, d.h. konkrete Daten sollen hinter der Schnittstelle verborgen bleiben. Die Schnittstellenoperationen nennen wir auch Dienste (service). Es ist softwaretechnisch sinnvoll, Dienste zu teilen in Abfragen (query), die eine Information liefern, aber nichts verändern, und Aktionen (action), die etwas verändern, aber keine Information liefern. Die Implementation einer Klasse besteht aus geschützten Daten und den in den Operationen gekapselten Algorithmen. Exemplare von Klassen heißen Objekte. Jedes Objekt hat eigene Exemplare der Daten, aber alle Objekte einer Klasse benutzen dieselben Operationen. 1 Peter Pepper: Grundlagen der Informatik. R. Oldenbourg Verlag, München Wien (1992) S. 146 v. 355. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 6 – Seite 1 von 22 6–2 6 Klassen und objektorientierte Konzepte Die wichtigen Beziehung zwischen Klassen sind die Benutzungs- (use) oder Kunde-Lieferant-Beziehung (client-supplier): Jede Klasse ist Lieferant von Diensten, die von Kundenklassen benutzt werden können; die Vererbungs- (inheritance) oder Oberklasse-Unterklasse-Beziehung (superclass-subclass): Jede Klasse kann als Oberklasse für von ihr erbende Unterklassen dienen. Im Vererbungsbegriff steckt eine genetische Metapher, die nicht durchweg passt. Angemessener scheint der neutralere Begriff der Erweiterung (extension), der sich jedoch nicht durchgängig etabliert hat. Component Pascal, C++ Beide Sprachen sind hybride Sprachen, die objektorientiertes, aber auch modulares bzw. prozedurales Programmieren ermöglichen. Eiffel, Java, C# Als rein objektorientierte Sprachen unterstützen Eiffel, Java und C# objektorientiertes Programmieren. Das bedeutet nicht, dass jedes Programm in diesen Sprachen automatisch ein gutes objektorientiertes Programm ist. Schließlich kann man eine Problemlösung mit einer einzigen Klasse realisieren, die intern prozedural strukturiert ist. Dieser Programmierstil heisst POOP (von procedural oder poor object-oriented programming). © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.2 Klassen 6–3 Tabelle 6.1 Bezeichnungsweisen Component Pascal Eiffel C++ Java erweiterbarer Typ mit typgebundenen Prozeduren (extensible type with type-bound procedures) Klasse (class) Variable eines erweiterbaren... (variable of an extensible...) Objekt (object), Exemplar einer Klasse (instance of a class) C# Typerweiterung (type extension) Vererbung (inheritance) Ableitung (derivation) Erweiterung (extension) Vererbung, Ableitung Basistyp (base type) Vorfahre, Vorgänger (parent, ancestor) Basisklasse (base class) Oberklasse (superclass) Basisklasse erweiterter Typ (extension) Erbe, Nachfolger (heir, descendant) abgeleitete Klasse (derived class) Unterklasse (subclass) abgeleitete Klasse - Merkmal (feature) Feld (field) Attribut (attribute) (Daten-)Element (data member) Feld typgebundene Prozedur, Methode (type-bound procedure) Routine (routine) Elementfunktion (member function) Methode (method) gewöhnliche Prozedur (proper procedure) Prozedur (procedure) Funktionsprozedur (function procedure) Element (member) void-Funktion (void function) Funktion (function) redefinieren (redefine) abstrakte Klasse, Prozedur (abstract class, procedure) aufgeschobene Klasse, Routine (deferred class, routine) - generische Klasse (generic class) überschreiben (override) abstrakte Klasse, Funktion (abstract class, function) Schablone (template class) generische Klasse Component Pascal Eine Klasse ist ein programmiererdefinierter Typ mit typgebundenen Prozeduren. Als Konstrukt dient der konventionelle Verbund. Es gibt auch andere Typen, Variablen und „normale“ Prozeduren, die nicht an einen Typ gebunden sind. Eiffel, C# Eine Klasse ist ein sprach- oder programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt keine Typen, die nicht von einer Klasse abgeleitet sind und keine Merkmale, die nicht zu einer Klasse gehören. C++ Eine Klasse ist ein programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt auch andere Typen, Variablen und „normale“ Funktionen, die nicht zu einer Klasse gehören. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6–4 6 Klassen und objektorientierte Konzepte Java Eine Klasse ist ein sprach- oder programmiererdefinierter Typ. Es gibt dafür ein spezielles Konstrukt. Es gibt auch andere Typen. Es gibt keine Variablen und Funktionen, die nicht zu einer Klasse gehören. Tabelle 6.2 Eigenschaften von Klassen Eigenschaft Klasse realisierbar mit Sprachkonstrukt spezielles Klassenkonstrukt Verhältnis zu Typsystem nichtgenerische Klasse ist generische Klasse Component Pascal Eiffel C++ Java C# RECORD class struct union class class struct class nein programmiererdefinierter Typ - ja sprach- oder programmiererdefinierter Typ programmiererdefinierter Typ beschreibt Menge programmiererdefinierter Typen Nicht-Klassentypen klassenexterne Daten ja ja nein klassenexterne Operationen 6.2.1 sprach- oder programmiererdefinierter Typ ja nein nein Bestandteile von Klassen Attribute Methoden Konstruktoren Destruktoren 6.2.2 Verhältnis zu Kompilationseinheiten Bei allen fünf Sprachen ist der Inhalt einer Datei Kompilationseinheit. Component Pascal Eine Klasse ist vollständig in einem Modul enthalten. Ein Modul kann mehrere Klassen enthalten. Kompilationseinheit ist das Modul. Eiffel Klassen sind die einzigen Kompilationseinheiten. C++ Die Definition einer Klasse steht üblicherweise in einer Schnittstellendatei, die Definitionen der Elementfunktionen der Klasse in der zugehörigen Implementationsdatei, außer bei generischen Klassen. Eine Datei kann mehrere Klassendefinitionen enthalten. Die Teile einer Klasse können auf mehrere Dateien verteilt werden. Beispielsweise könnte jede Funktionsdefinition in einer anderen Datei stehen; die Klassendefinition könnte mit Inkludierung aus verstreuten Teilen zusammengesetzt sein. Vermutlich ist es sinnvoll, von den vielfältigen Verteilungsmöglichkeiten nur wenige zu nutzen, z.B. die erstgenannte. Java, C# In einer Datei können mehrere Klassen aufeinander folgen und eine Kompilationseinheit bilden. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.3 Objekte 6.3 6–5 Objekte Allgemein können Variablen in folgenden drei Speicherbereichen liegen: außerhalb von Routinen und Klassen statisch vereinbarte Variablen im globalen Datenbereich; in Routinen statisch vereinbarte Variablen (und Parameter) im Laufzeitkeller (runtime stack); dynamisch erzeugte Variablen auf der Halde (heap). Bei statisch vereinbarten Variablen im globalen Datenbereich und im Laufzeitkeller sprechen wir von Wertsemantik, bei dynamisch erzeugten Variablen auf der Halde von Referenzsemantik. Die Programmiersprachen unterscheiden sich darin, in welchen Speicherbereichen Objekte liegen können. Alle Sprachen können Objekte einer Klasse dynamisch erzeugen. Component Pascal und C++ können von derselben Klasse Objekte statisch vereinbaren oder dynamisch erzeugen. Eiffel und C# kennen Klassen mit Referenzsemantik und Klassen mit Wertsemantik. Eiffel kann von einer Klasse mit Referenzsemantik ein Wertobjekt vereinbaren, aber nur, wenn die Klasse genau eine parameterlose Erzeugungsprozedur hat. In C# sind Klassen mit Wertsemantik nicht beerbbar oder erbend, d.h. es sind nur ADTen. Tabelle 6.3 Wert- und Referenzsemantik Eigenschaft Component Pascal statisches Wertobjekt (globaler Datenbereich, Keller) Eiffel C++ ja dynamisches Referenzobjekt (Halde) Name für aktuelles Objekt (von dem ein Dienst aufgerufen ist) Java C# nein ja ja frei wählbar Referenzparameter von Objekt oder Wertparameter von Zeiger auf Objekt Current this this Referenz Zeiger Referenz Wir zeigen anhand der Datumsklasse aus 2.3 S. 2-26 beispielhaft, wie man zu einem Wertobjekt und einem Referenzobjekt kommt und damit umgeht. 6.3.1 Component Pascal Beispiel für Wertobjekt VAR date : DateDesc; ... date.Init; date.Set (1997, 3, 7); date.Advance; (* RECORD *) Eiffel date : expanded DATE ... date.set (1997, 3, 7) date.advance -- Standardinitialisierung mit make C++ Date date; ... date.set(1997, 3, 7); date.advance; // Standardinitialisierung mit Date () Java 27.9.12 (* Explizite Initialisierung *) In Java gibt es keine Wertobjekte. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6–6 6 Klassen und objektorientierte Konzepte C# In C# gibt es zwar Wertobjekte, aber nur als Exemplare von Typen, die mit dem struct-Konstrukt definiert sind. In der Definition wäre also class durch struct zu ersetzen. struct-Klassen sind nicht beerbbar. Der Programmierer muss schon bei der Definition der Klasse entscheiden, ob es davon nur Wertobjekte oder nur Referenzobjekte geben soll. Dies widerspricht dem Hellseherprinzip. 6.3.2 Beispiel für Referenzobjekt Component Pascal VAR date : Date; ... NEW (date); date^.Init; date.Set (1997, 3, 7); date.Advance; Eiffel (* POINTER TO RECORD *) (* Explizite Dereferenzierung *) (* Implizite Dereferenzierung *) date : DATE ... create date.make date.set (1997, 3, 7) date.advance C++ -- Explizite Erzeugung und Initialisierung -- Implizite Dereferenzierung Mit Referenz: Date & date = Date(); ... date.set(1997, 3, 7); date.advance(); // Explizite Erzeugung und Initialisierung // Implizite Dereferenzierung Mit Zeiger: Date * date = new Date(); ... (*date).set(1997, 3, 7); date->advance(); Java, C# Date date = new Date(); ... date.set(1997, 3, 7); date.advance(); 6.3.3 // Explizite Erzeugung und Initialisierung // Explizite Dereferenzierung // Explizite Dereferenzierung // Explizite Erzeugung und Initialisierung // Implizite Dereferenzierung Zugriffe auf Objektmerkmale obj bezeichnet jeweils ein Objekt, fea ein Merkmal. Tabelle 6.4 Zugriffe auf Objektmerkmale Zugriff auf Component Pascal Eiffel C++ Attribut eines Wertobjekts obj.fea obj.fea obj.fea parameterlose Funktion eines Wertobjekts obj.fea () obj.fea obj.fea() Attribut eines Referenzobjekts obj^.fea obj.fea obj.fea obj^.fea () obj.fea () obj.fea parameterlose Funktion eines Referenzobjekts Java C# obj.fea obj.fea() Zeiger: (*obj).fea obj->fea obj.fea obj.fea obj.fea() obj.fea() Referenz: obj.fea Zeiger: (*obj).fea() obj->fea() Referenz: obj.fea() Dem Konzept der Bezugstransparenz, d.h. der Unabhängigkeit des Zugriffs von der Implementation, entspricht offenbar Eiffel am besten. Die Implementationen des © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.4 Klassendaten und -operationen 6–7 Objekts (statisch/dynamisch) und des Merkmals (Attribut/Funktion) können ohne Auswirkung auf die Benutzung geändert werden. 6.4 Klassendaten und -operationen 6.4.1 Klassendaten Die Definition einer Klasse beschreibt, welche Attribute jedes Objekt dieser Klasse hat und welche Operationen mit diesem Objekt ausführbar sind. Attribute beschreiben also normalerweise Objektdaten, d.h. es wird exemplarweise Speicherplatz für sie belegt. Manchmal braucht man Attribute, die für alle Objekte nur einmal vorhanden sind, also der Klasse zugeordnet sind, diese nennen wir Klassendaten. Es wird nur einmal Speicherplatz für sie belegt, alle Objekte arbeiten mit dem gemeinsamen Speicherplatz, d.h. Wert des Attributs. Component Pascal Es gibt kein spezielles Konstrukt für Klassendaten. Man realisiert sie am besten als nichtexportierte Variable in dem Modul, das die Klassendefinition enthält. MODULE M; TYPE AClass* = RECORD ... END; VAR classData : SomeType; PROCEDURE (VAR a : AClass) Proc* (...); BEGIN Zugriff auf classData und auf Objektdaten; END Proc; BEGIN Initialisierung von classData; END M. Eiffel Es gibt kein spezielles Konstrukt für Klassendaten. Man realisiert sie am besten mit einer geschützten Einmalfunktion. Die Klassendaten werden einmal dynamisch erzeugt, jedes Objekt erhält bei jedem Zugriff eine Referenz auf dasselbe Exemplar der Klassendaten. class A_CLASS feature {NONE} class_data : SOME_TYPE once create Result.make (...) end -- class_data feature proc (...) do Zugriff auf class_data und auf Objektdaten end -- proc end -- class A_CLASS C++ Klassendaten sind mit dem Speicherklassenspezifikator static realisierbar. class A_class { protected: static Some_type class_data; public: void proc (...); }; Some_type A_class::class_data = Initialisierung; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6–8 6 Klassen und objektorientierte Konzepte void A_class::proc (...) { Zugriff auf class_data und auf Objektdaten; } Java Klassendaten sind mit dem Speicherklassenmodifikator static realisierbar. class AClass { private static SomeType classData = Initialisierung; public void proc (...) { Zugriff auf classData und auf Objektdaten; } } C# Klassendaten sind mit dem Speicherklassenmodifikator static realisierbar. class AClass { protected static SomeType classData = Initialisierung; public void Proc (...) { Zugriff auf classData und auf Objektdaten; } } 6.4.2 Klassenoperationen Unterstützt eine Sprache Klassendaten, so kann eine Klasse Operationen haben, die nur auf Klassendaten, nicht auf Objektdaten zugreifen. Wir nennen solche Operatioen Klassenoperationen. Für sie macht es Sinn, sie an die Klasse, nicht ein Objekt zu binden, d.h. sie mit dem Klassennamen, nicht einem Objektnamen qualifiziert aufzurufen. Component Pascal Es gibt kein spezielles Konstrukt für Klassenoperationen. Man realisiert sie am besten als Prozeduren in dem Modul, das die Klassendefinition enthält. MODULE M; TYPE AClass* = RECORD ... END; VAR classData : SomeType; PROCEDURE ClassProc* (...); BEGIN Zugriff nur auf classData; END ClassProc; BEGIN Initialisierung von classData; END M. Benutzung: M.ClassProc (...); Eiffel Es gibt kein Konstrukt für Klassenoperationen. Man realisiert sie als normale Objektoperationen. Zu ihrer Benutzung muss stets ein Objekt erzeugt werden. C++ Der Spezifikator static ist auch auf Elementfunktionen anwendbar. Die Funktion kann dann mit dem Klassennamen qualifiziert aufgerufen werden, ohne dass ein Objekt dieser Klasse existieren muss. class A_class { protected: static Some_type class_data; public: static void class_proc (...); }; Some_type A_class::class_data = Initialisierung; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.5 Abstrakte Klassen 6–9 void A_class::class_proc (...) { Zugriff nur auf class_data; } Benutzung: A_class::class_proc (...); Java, C# Klassenfunktionen sind mit dem Speicherklassenmodifikator static realisierbar. class AClass { protected static SomeType classData = Initialisierung; public static void classProc (...) { Zugriff nur auf classData; } } Benutzung: AClass.classProc (...); 6.5 Abstrakte Klassen 6.5.1 Gemeinsames Alle fünf Sprachen unterstützen abstrakte Dienste und Klassen. Ein Dienst ist abstrakt (abstract), wenn seine Signatur ohne eine zugeordnete Implementation deklariert ist. Eine Klasse ist abstrakt, wenn sie wenigstens einen abstrakten Dienst enthält. Von einer abstrakten Klasse können keine Objekte erzeugt werden. Abstrakte Klassen dienen dazu, Schnittstellen und partielle Implementationen für Unterklassen festzulegen. Sie sind ein wichiger Aspekt des zentralen Abstraktionskonzepts der Vererbung. 6.5.2 Unterschiedliches Component Pascal Das reservierte Wort ABSTRACT markiert abstrakte Prozeduren und Klassen. Eine Klasse ist abstrakt, wenn sie wenigstens eine abstrakte Prozedur enthält. TYPE AnAbstractClass* = ABSTRACT RECORD ... END; PROCEDURE (VAR a : AnAbstractClass) DoSomething* (...), ABSTRACT; Ein Übersetzer braucht die zusätzliche Markierung des Typs mit ABSTRACT nicht; diese verbessert die Lesbarkeit. Eiffel Das reservierte Wort deferred markiert abstrakte (aufgeschobene) Routinen und Klassen. Eine Klasse ist aufgeschoben, wenn sie wenigstens eine aufgeschobene Routine enthält. deferred class AN_ABSTRACT_CLASS feature do_something (...) deferred end end -- class AN_ABSTRACT_CLASS C++ Der Pur-Spezifikator „= 0“ markiert abstrakte Funktionen. Eine Klasse ist abstrakt, wenn sie wenigstens eine pure virtuelle Funktion enthält. class An_abstract_class { public: virtual void do_something (...) = 0; }; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 10 6 Klassen und objektorientierte Konzepte Stroustrup wählte die irritierende pseudoalgebraische Notation „= 0“ zur Markierung fehlender Implementationen, um kein weiteres Schlüsselwort (etwa abstract) einführen zu müssen [Str94a]. Der Kopf einer abstrakten Klasse wird nicht zusätzlich markiert. Java, C# Das reservierte Wort abstract markiert abstrakte Methoden und Klassen. Eine Klasse ist abstrakt, wenn sie wenigstens eine abstrakte Methode enthält. public abstract class AnAbstractClass { public abstract void doSomething (...); } 6.6 Vererbung Tabelle 6.5 Vererbung Eigenschaft Component Pascal Eiffel C++ Java C# Art der Vererbung (Anzahl Oberklassen) einfach mehrfach, wiederholt mehrfach modulextern: durch Exportmarken des Basistyps gesteuert vollständig durch Zugriffsmodifikator private auf Basisklasse einschränkbar ABSTRACT deferred einfach, Schnittstellen mehrfach modulintern: vollständig Zugriff auf geerbte Merkmale Konstrukt für abstrakten Dienst/abstrakte Klasse Konstrukt für Schnittstelle nein ANYREC ANYPTR ANY Unterklasse aller Klassen nein NONE Baum vollständiger Verband Component Pascal abstract interface I {...} Oberklasse aller Klassen Vererbungsstruktur =0 nein Object nein Halbordnung halbvollständiger Verband TYPE A = RECORD Felder für A END; Prozeduren für A TYPE B = RECORD (A) zusätzliche Felder für B END; zusätzliche und redefinierte Prozeduren für B Eiffel class A Merkmale für A end © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.7 Anpassung geerbter Merkmale 6 – 11 class B inherit A redefine Redefinitionsdeklarationen end feature zusätzliche und redeklarierte Merkmale für B end C++ class A { Elemente für A }; Elementfunktionsdefinitionen für A class B : public A { zusätzliche Elemente und Definitionen redefinierter Elementfunktionsdefinitionen für B }; zusätzliche und redefinierte Elementfunktionsdefinitionen für B Java public class A { Elemente für A } public class B extends A { zusätzliche und überschriebene Elemente für B } C# public class A { Elemente für A } public class B : A { zusätzliche und überschriebene Elemente für B } 6.7 Anpassung geerbter Merkmale Welche Möglichkeiten hat eine Unterklasse, ein von einer Oberklasse geerbtes Merkmal für ihre Zwecke anzupassen? Die wichtigste Möglichkeit ist die Redefinition geerbter Operationen. Diese ist in allen fünf Sprachen vorhanden. Die Redefinitionsregeln sind in 6.9 genannt. Component Pascal Typgebundene Prozeduren können in Erweiterungstypen redefiniert werden, wenn sie im Basistyp als abstrakt (ABSTRACT) oder erweiterbar (EXTENSIBLE) markiert sind. Möglichkeiten: Definieren einer abstrakten typgebundenen Prozedur Redefinieren einer erweiterbaren typgebundenen Prozedur Typgebundene Prozeduren können als nur redefinierbar exportiert werden (Exportmarke „-“). Der erweiternde Typ darf solche Prozeduren redefinieren, aber nicht selbst aufrufen. Sinnvoll ist das für die Implementation von Schablonenmethoden, die oft in Frameworks vorkommen. Dies ist ein Beispiel dafür, wie Sprachkonstrukte Entwurfsmuster unterstützen können. kovariantes Ändern des Ergebnistyps einer typgebundenen Funktion Einschränken des Exportstatus eines Merkmals (zum Erzwingen polymorpher Benutzung), sofern beide Typen im selben Modul vereinbart sind Entziehen der Redefinierbarkeit einer typgebundenen Prozedur (durch fehlendes EXTENSIBLE) 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 12 Eiffel 6 Klassen und objektorientierte Konzepte Jede geerbte Routine kann redefiniert werden, es sei denn, sie ist in der Vorgängerklasse als gefroren spezifiziert (frozen). Möglichkeiten: Definieren (Effektivieren) einer abstrakten Routine Redefinieren einer konkreten Routine Ändern des Vertrags einer Routine: Vorbedingung abschwächen, Nachbedingung verstärken kovariantes Ändern der Signatur einer Routine Redefinieren einer Funktion als Attribut Aufheben der Definition einer Routine Ändern des Exportstatus eines Merkmals Umbenennen eines Merkmals Verschmelzen mehrerer Merkmale gleicher Signatur zu einem Merkmal Replizieren eines Merkmals Entziehen der Redefinierbarkeit eines Merkmals (mit frozen) Es zeigt sich, dass Eiffel hier als rein objektorientierte Sprache viele Möglichkeiten bietet. Diese sind aber für den Anfang und viele Anwendungen nicht unbedingt erforderlich; bei ungeschickter Programmierung können sie auch Probleme verursachen. C++ Eine geerbte Elementfunktion kann redefiniert (überschrieben) werden, wenn sie in der Basisklasse, in der sie zuerst definiert ist, als virtuell spezifiziert ist (virtual). Möglichkeiten: Java Definieren, d.h. Konkretisieren einer abstrakten (puren virtuellen) Elementfunktion Überschreiben einer konkreten virtuellen Elementfunktion Redefinieren einer konkreten virtuellen Elementfunktion als abstrakt kovariantes Ändern des Ergebnistyps einer virtuellen Elementfunktion Einschränken des Exportstatus eines Elements (mit protected, private) Eine geerbte Methode kann redefiniert (überschrieben) werden, es sei denn, sie ist in der Oberklasse als final spezifiziert (final). Möglichkeiten: C# Definieren, d.h. Konkretisieren einer abstrakten Methode Überschreiben einer konkreten Methode kovariantes Ändern des Ergebnistyps einer Methode Einschränken des Exportstatus einer Methode (mit protected, private) Entziehen der Redefinierbarkeit einer Methode (mit final) C# zu ergänzen. 6.8 Polymorphie und dynamisches Binden 6.8.1 Gemeinsames Polymorphie Alle fünf Sprachen bieten das zentrale objektorientierte Konzept der Polymorphie und des dynamischen Bindens: Bei polymorphen Größen können aufgerufene Dienste abhängig von ihrem dynamischen Typ dynamisch gebunden werden. Eine Größe ist eine parameterlose Abfrage (Attribut oder Funktion), ein Formalparameter oder eine Variable. Polymorphie bedeutet allgemein Vielgestaltigkeit, hier die Eigenschaft einer © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.8 Polymorphie und dynamisches Binden 6 – 13 Größe, verschiedene Gestalten anzunehmen. Eine Größe ist polymorph, wenn sie sich auf Objekte verschiedener Typen beziehen kann. Statischer und dynamischer Typ Alle fünf Sprachen sind statisch typisiert, d.h. sie binden jede Größe bei ihrer Vereinbarung an einen Typ – ihren statischen Typ, der zur Übersetzungszeit festgelegt ist. Polymorphe Größen sind zudem zur Laufzeit an einen dynamischen Typ gebunden. Der dynamische Typ einer Größe steht in einer Vererbungsbeziehung zu ihrem statischen Typ: Der dynamische Typ kann ein Untertyp ihres statischen Typs sein. Polymorph oder nicht? Welche Größen können polymorph sein? Eine Größe mit Wertsemantik bezieht sich stets auf ein festes Wertobjekt ihres statischen Typs; bei ihr sind also statischer und dynamischer Typ identisch. Daher sind Wertgrößen nicht polymorph. Nur Referenzund Zeigergrößen können polymorph sein, nur bei diesen können sich statischer und dynamischer Typ unterscheiden, nur diese lassen sich zur Laufzeit an Referenzobjekte verschiedener Untertypen ihres statischen Typs binden. Semantik zählt Im Folgenden bedeutet Aufruf mit Semantik dynamischen Bindens nur, dass der Effekt des Aufrufs wie unter dynamischem Binden ist. Daraus folgt nicht, dass unbedingt dynamisch gebunden wird. Wo es ohne Änderung der Semantik möglich ist, kann ein Kompilierer einen Aufruf auch statisch binden, z.B. wenn die Zielgröße des Aufrufs nicht polymorph oder der aufgerufene Dienst nicht redefinierbar ist. Das Konzept der Polymorphie und des dynamisches Bindens ermöglicht flexibel anpassbare und erweiterbare Komponenten. Abgeschlossene, vollständig implementierte Lieferanten können Kunden mit spezifischen Aufträgen bedienen, indem die Kunden von Lieferanten spezifizierte Dienste in eigenen Unterklassen redefinieren und den Lieferanten eigene Objekte zur Bearbeitung übergeben. Viele objektorientierte Entwurfsmuster beruhen auf Polymorphie und dynamischem Binden [GHJV04]. 6.8.2 Historisches Schon Simual 67, die erste objektorientierte Sprache, führt das Konzept der Polymorphie und des dynamisches Bindens ein, allerdings nicht als Standard, sondern als Option neben statischem Binden. Smalltalk erhebt Polymorphie und dynamisches Binden zum Standard, allerdings in rein dynamisch typisiertem Kontext. Oberon-2, Component Pascal und Eiffel verbinden die Standardsemantik dynamischen Bindens mit statischer Typisierung; Java folgt dieser Entwicklungslinie. C++ und Borlands Object Pascal übernehmen das Doppelmodell von Simula 67; C# folgt ihnen dabei. 6.8.3 Component Pascal Beim Aufruf einer typgebundenen Prozedur gilt genau dann die Semantik dynamischen Bindens, wenn auf das Objekt mit einem Referenzparameter oder einer Zeigervariablen zugegriffen wird. 6.8.4 Eiffel Beim Aufruf von Merkmalen gilt generell die Semantik dynamischen Bindens. Vereinbarung deferred class A -- deferred means abstract. feature f deferred end -- implicitly redefinable, to be dynamically bound g do print ("A.g") end -- implicitly redefinable, to be dynamically bound end -- class A class B inherit A redefine g end 27.9.12 -- f may be listed, but must not. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 14 6 Klassen und objektorientierte Konzepte feature f do print ("B.f") end g do print ("B.g") end -- redeclares, effects A.f -- redeclares, redefines A.g end -- class B class C inherit B redefine f, g end feature f do print ("C.f") end g do print ("C.g") end -- redeclares, redefines B.f -- redeclares, redefines B.g end -- class C Benutzung a:A ... -- polymorphic entity a of static type A create {B} a a.f a.g -- dynamic type of a is B -- dynamically bound to B.f -- dynamically bound to B.g create {C} a a.f a.g -- dynamic type of a is C -- dynamically bound to C.f -- dynamically bound to C.g Ein in großen Softwareprojekten praktisch auftretendes Problem ist versehentliches Redefinieren einer konkreten Routine. Als Folge kann sich durch polymorphe Aufrufe das Systemverhalten ändern, ohne dass die Ursache leicht erkennbar ist. Das Problem versehentlichen Redefinierens löst der redefine-Abschnitt: Redefinieren ohne explizite redefine-Deklaration ist ein Kontextfehler. Ist die Vorgängerversion der Routine abstrakt, so kann eine redefine-Deklaration hingeschrieben werden, muss aber nicht. Warum nicht? Eine abstrakte Routine ist nicht ausführbar; versehentliches Effektivieren einer abstrakten Routine kann kein anderes Systemverhalten bewirken. 6.8.5 C++ Beim Aufruf einer Elementfunktion gilt die Semantik dynamischen Bindens, wenn gilt: (1) (2) (3) (4) (5) Auf das Objekt wird mit einer Referenz oder einem Zeiger q des statischen Typs A zugegriffen, dabei wird eine Funktion namens f aufgerufen. In A oder einer Basisklasse von A ist eine als virtual spezifizierte Funktion f (die damit weder eine Klassenfunktion noch eine Inline-Funktion ist) mit der Parameterliste P definiert, E sei ihr Ergebnistyp. Der dynamische Typ von q sei B. B ist eine von A abgeleitete Klasse, und in B oder einer von A abgeleiteten Basisklasse von B ist eine Funktion f mit der Parameterliste P und dem Ergebnistyp F definiert. F ist gleich E oder eine von E abgeleitete Klasse. Die aktuellen Parameter des Aufrufs von f über q sind mit den formalen Parametern der in A gültigen Definition von f kompatibel. Unter diesen Voraussetzungen wird die in B gültige Version von f aufgerufen. Vereinbarung class A { public: virtual void f () = 0; virtual void g (); -- abstract, overridable, to be dynamically bound -- overridable, to be dynamically bound © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.8 Polymorphie und dynamisches Binden 6 – 15 void h (); -- not overridable, to be statically bound } void A::g () { cout << "A::g"; } void A::h () { cout << "A::h"; } class B : public A { public: void f (); -- overrides A::f, still virtual, to be dynamically bound void g (); -- overrides A::g, still virtual, to be dynamically bound void h (); -- hides A::h, to be statically bound } void B::f () { cout << "B::f"; } void B::g () { cout << "B::g"; } void B::h () { cout << "B::h"; } class C : public B { public: virtual void f (); -- overrides B::f, still virtual, to be dynamically bound void g (); -- overrides B::g, still virtual, to be dynamically bound virtual void h (); -- hides B::h, to be dynamically bound via C-pointers } void C::f () { cout << "C::f"; } void C::g () { cout << "C::g"; } void C::h () { cout << "C::h"; } Benutzung A * a; ... -- polymorphic pointer a of static type A* a = new B(); a->f(); a->g(); a->h(); -- dynamic type of a is B* -- dynamically bound to B::f -- dynamically bound to B::g -- statically bound to A::h a = new C(); a->f(); a->g(); a->h(); -- dynamic type of a is C* -- dynamically bound to C::f -- dynamically bound to C::g -- statically bound to A::h Aus dem Quelltext der Klassen B und C ist nicht ersichtlich, welche Funktionen überschreiben (f, g) und welche verdecken (h), und bei welchen wann dynamisch oder statisch gebunden wird. Gemischtes Vorkommen von virtuellen, dynamisch gebundenen und nichtvirtuellen, statisch gebundenen Funktionen kann verwirren. Welches Problem wäre mit nichtvirtuellen, statisch gebundenen Funktionen zu lösen? Dem Autor ist keins bekannt. Kein objektorientiertes Entwurfsmuster beruht auf statischem Binden [GHJV04]. Eine in einer abgeleiteten Klasse definierte Funktion kann versehentlich eine geerbte virtuelle Funktion überschreiben, da weder Neudefinieren noch Überschreiben markiert werden muss. Auch das gemischte Vorkommen von virtuellen und überladenen Funktionen kann verwirren, da überladene Funktionen statisch gebunden werden. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 16 Leitlinie 6.1 C++: Vermeide Verdecken in abgeleiteten Klassen 6 Klassen und objektorientierte Konzepte Die Vereinbarung eines Elements in einer abgeleiteten Klasse verdeckt die Vereinbarung eines gleichnamigen Elements in einer Basisklasse. Vermeide diese spezielle Art des Verdeckens, da sie verwirren und versehentlich zu statischem Binden führen kann, wo dynamisches Binden erwartet wird! Leitlinie 6.2 C++: Vermeide Überladen virtueller Funktionen Vermeide wenigstens das Überladen virtueller Funktionen mit nichtvirtuellen Funktionen! Leitlinie 6.3 C++: Vereinbare Elementfunktionen virtuell Vereinbare neue Elementfunktionen wo möglich virtuell, damit sie überschreibbar sind und die Semantik dynamischen Bindens gilt! Ausgenommen sind statische und Inline-Funktionen. 6.8.6 Java Beim Aufruf von Methoden – außer bei (mit static vereinbarten) Klassenmethoden – gilt die Semantik dynamischen Bindens. Vereinbarung public abstract class A { public abstract void f (); -- abstract, overridable, to be dynamically bound public void g () { System.out.print ("A.g"); }; -- overridable, to be dynamically bound } public class B extends A { @Override public void f () { System.out.print ("B.f"); }; -- overrides A.f, to be dynamically bound @Override public void g () { System.out.print ("B.g"); }; -- overrides A.g, to be dynamically bound } public class C extends B { @Override public void f () { System.out.print ("C.f"); }; -- overrides B.f, to be dynamically bound @Override public void g () { System.out.print ("C.g"); }; -- overrides B.g, to be dynamically bound } Benutzung A a; ... -- polymorphic reference a of static type A a = new B(); a.f(); a.g(); -- dynamic type of a is B -- dynamically bound to B.f -- dynamically bound to B.g a = new C(); a.f(); a.g(); -- dynamic type of a is C -- dynamically bound to C.f -- dynamically bound to C.g Obwohl in Java wie in Component Pascal und Eiffel die Semantik dynamischen Bindens Standard ist, können in diesem Zusammenhang statisches Binden von Klassenmethoden und Verdecken, Überladen, Überschatten und Verdunkeln von Namen zu Verwirrungen führen. Zwar informiert die optionale @Override-Annotation ähnlich wie Eiffels redefine-Abschnitt den Kompilierer, dass die Methode eine geerbte Methode © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.8 Polymorphie und dynamisches Binden 6 – 17 überschreiben soll, sodass er einen Fehler melden kann, falls keine Oberklasse eine passende Methode hat. Während jedoch Eiffel das Verdecken von Namen verbietet und damit auch den Fehler erkennt, dass in einer Unterklasse ein vermeintlich neues Merkmal definiert wird, wo schon ein gleichnamiges in einer Oberklasse existiert, kann die @Override-Annotation diesen Fehler nicht erkennen. So bleibt versehentliches Verdecken und Neudefinieren von Namen eine Fehlerquelle in Java. Leitlinie 6.4 Java: Benutze @Override Leitlinie 6.5 Java: Vermeide Verwirrendes 6.8.7 Markiere überschreibende Methoden mit der @Override-Annotation, da dies die Lesbarkeit verbessert und Fehler vermeiden hilft! Vermeide Verdecken, Überschatten und Verdunkeln von Namen, da sie verwirren! Vermeide möglichst auch Überladen, da es im Zusammenhang mit Überschreiben verwirren kann! Studiere die Beispiele in [BlN05]! C# C# bietet ähnlich wie Simula 67 und C++ sowohl statisches als auch dynamisches Binden. Vereinbarung public abstract class A { public abstract void f (); -- abstract, overridable, to be dynamically bound public virtual void g () { System.Console.Write ("A.g"); }; -- overridable, to be dynamically bound public void h () { System.Console.Write ("A.h"); }; -- not overridable, to be statically bound } public class B : A { public override void f () { System.Console.Write ("B.f"); }; -- overrides A.f, to be dynamically bound public override void g () { System.Console.Write ("B.g"); }; -- overrides A.g, to be dynamically bound public new void h () { System.Console.Write ("B.h"); }; -- hides A.h, to be statically bound } public class C extends B { public override void f () { System.Console.Write ("C.f"); }; -- overrides B.f, to be dynamically bound public void g () { System.Console.Write ("C.g"); }; -- hides B.g, warning issued, statically bound public virtual void h () { System.Console.Write ("C.g"); }; -- hides B.g, warning issued, dynamically bound via C-variables } Benutzung 27.9.12 A a; ... -- polymorphic reference a of static type A a = new B(); a.f(); a.g(); a.h(); ----- dynamic type of a is B dynamically bound to B.f dynamically bound to B.g statically bound to A.h a = new C(); a.f(); a.g(); a.h(); ----- dynamic type of a is C dynamically bound to C.f dynamically bound to C.g statically bound to A.h © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 18 6 Klassen und objektorientierte Konzepte C# folgt hier eher C++ als Java, stopft aber die Fehlerquellen versehentliches Überschreiben, Neudefinieren und Verdecken. Dennoch bleiben folgende Probleme offen: Gemischtes Vorkommen von virtuellen, dynamisch gebundenen und nichtvirtuellen, statisch gebundenen Methoden kann verwirren. Welches Problem wäre mit nichtvirtuellen, statisch gebundenen Methoden zu lösen? Kein objektorientiertes Entwurfsmuster beruht auf statischem Binden [GHJV04]. 6.9 Redefinitionsregeln Sei A eine Klasse, in der die Operation op definiert ist. Sei B eine Unterklasse von A, in der op mit demselben Namen redefiniert ist. Component Pascal, C++ Es gilt die Invarianzregel für die Parameter und die Kovarianzregel für das Ergebnis von op. Das heißt: op in A und op in B haben dieselbe Anzahl von Parametern, und die Typen der Parameter sind gleich. Der Ergebnistyp von op in B ist eine Erweiterung bzw. Ableitung des Ergebnistyps von op in A. Eiffel Es gilt die Kovarianzregel für die Parameter und das Ergebnis von op. Das heißt: op in A und op in B haben dieselbe Anzahl von Parametern, und die Typen der Parameter und des Ergebnisses von op in B sind jeweils Unterklassen der entsprechenden Typen von op in A. C++ In früheren Versionen von C++ galt auch für das Funktionsergebnis die Invarianzregel. Hier soll auch auf einige Besonderheiten von C++ hingewiesen werden. In A und B können Funktionen mit demselben Namen, aber unterschiedlichen Parameterlisten definiert sein. In diesem Fall handelt es sich nicht um Redefinition, sondern um Überladen (overloading). Die Mischung der Mechanismen Redefinition und Überladen – insbesondere verquickt mit dem Mechanismus der Standardparameter – kann zu Verwirrung beitragen. Java Java zu ergänzen. C# C# zu ergänzen. 6.10 Zugriffe auf Oberklassenversionen geerbter Operationen Das folgende Problem stellt sich oft: Sei A eine Klasse, in der die Operation op definiert ist. Sei B eine direkte Unterklasse von A, in der op mit demselben Namen redefiniert ist. Wie kann die B-Version von op die A-Version von op aufrufen? Component Pascal Die in der Erweiterungslinie unmittelbare Vorgängerversion von op kann durch Anhängen von ^ an den Namen relativ bezeichnet werden: op^ (...); Dieser Mechanismus stammt von Smalltalk. Da in einer Operation Vorgängerversionen beliebiger anderer Operationen aufrufbar sind, ist der Mechanismus mächtiger als das Problem verlangt, er gefährdet dadurch aber das Geheimnisprinzip. Eiffel In frühen Eiffel-Versionen mussten die Mechanismen wiederholtes Erben und Umbenennen geerbter Merkmale eingesetzt werden, um die unmittelbare Vorgängerversion von op bezeichnen zu können. class B © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.11 Schachtelung 6 – 19 inherit A rename op as op_A export {NONE} all end A redefine op select op end feature op (...) do ... op_A (...) ... end -- op end -- class B Dieses umständliche, durch rename-, export-, redefine- und select-Abschnitte schwergewichtige Muster wurde unnötig mit dem 1997 in den NICE-Standard eingeführten Precursor-Konstrukt. Die unmittelbare Vorgängerversion von op hat einfach den relativen Standardnamen Precursor: op (...) do ... Precursor (...) ... end -- op C++ Alle Vorgängerversionen von op können absolut durch Qualifizierung mit dem Klassennamen bezeichnet werden. A::op(...); Dieser Mechanismus ist einerseits mächtiger als der von Component Pascal. Andererseits zeigen sich seine Grenzen bei indirekter wiederholter Vererbung und bei Erweiterungen von Vererbungsstrukturen. Java Java bietet mit dem super-Konstrukt denselben Mechanismus wie Smalltalk und Component Pascal. Die unmittelbare Vorgängerversion von op kann durch Qualifizieren mit super relativ bezeichnet werden: super.op(...); C# C# zu ergänzen. 6.11 Schachtelung Hier geht es um die Frage, welche Strukturierungseinheiten textuell ineinandergeschachtelt werden können. Damit ist auch eine Steuerung der Sichtbarkeitsbereiche verbunden. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 20 6 Klassen und objektorientierte Konzepte Tabelle 6.6 Schachtelung Eigenschaft Component Pascal Prozedur in Prozedur ja nein nein - Modul in Modul Klasse in Klasse C++ Java nein Prozedur in Modul ja Operation in Klasse logisch ja, textuell nein Klasse in Modul Eiffel C# ja ja logisch u. Inline-Funktion ja, textuell bzw. sonst nein ja ja - 6.12 Speicherverwaltung Component Pascal, Eiffel, Java, C# Bei diesen Sprachen übernimmt das Laufzeitsystem die automatische Speicherbereinigung (garbage collection) für nicht mehr benutzte dynamisch erzeugte Objekte. C++ Es gibt keine automatische Speicherbereinigung. Die EntwicklerIn muss die Speicherverwaltung selbst programmieren, indem sie zu jeder Klasse geeignete Löschoperationen definiert. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 6.12 Speicherverwaltung 27.9.12 6 – 21 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 – 22 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 6 Klassen und objektorientierte Konzepte 27.9.12 7 Typen und Datenkonzepte Aufgabe 7 7.1 Gemeinsamkeiten Die Typsysteme der fünf Sprachen stimmen in einer Reihe von Eigenschaften überein. Typbindung: Jede Variable, jeder Parameter, jeder Ausdruck besitzt einen zur Kompilationszeit festgelegten, unveränderlichen Typ – den statischen Typ. Klassen und Typen: Das Klassenkonzept ist in das Typkonzept integriert. Jede Klasse beschreibt einen Typ. (Eine generische Klasse beschreibt eine Menge von Typen.) Es gibt sprachdefinierte und programmiererdefinierte Typen. Statische Typprüfung: Der Kompilierer prüft Typverträglichkeit in Ausdrücken, bei Zuweisungen, Parameterübergaben usw. Bei numerischen Basistypen gibt es Regeln zur impliziten Typanpassung. Größen, die sich auf Objekte (Exemplare von Klassen) beziehen, haben auch einen dynamischen Typ. Konformität: Der dynamische Typ einer Objekt-Größe muss einer Unterklasse ihres statischen Typs entsprechen. 7.2 Unterschiede Die Typsysteme der fünf Sprachen unterscheiden sich hinsichtlich der Antworten auf folgende Fragen. Gibt es Typen, die keiner Klasse entsprechen? Werden sprachdefinierte und programmiererdefinierte Typen völlig gleich behandelt? Wenn nein, welche Unterschiede gibt es? Wann ist ein Typ konform zu, äquivalent, kompatibel oder verträglich mit einem anderen? Wie lasch oder streng ist die statische Typprüfung? Welche Typen werden implizit angepasst? Nach welchen Regeln? Gibt es Möglichkeiten zur expliziten Typanpassung? Kann die Einhaltung von Wertebereichen zur Laufzeit überprüft werden? Bei welchen Größen macht es Sinn, von ihrem dynamischen Typ zu sprechen? Welche Typinformationen liegen zur Laufzeit vor? Welche Möglichkeiten für Typprüfungen zur Laufzeit gibt es? 7.3 Typsystem Component Pascal, C++ Klassen beschreiben spezielle Typen. Es gibt Typen, die nicht durch Klassen beschrieben sind. Die Typ-Spezialisierungsstruktur ist typisch für hybride Sprachen. Typen sprachdefinierte Typen (Component Pascal: predefined, C++: built-in), einfache Basistypen programmiererdefinierte Typen gewöhnliche Typen (Nicht-Klassen) © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 7 – Seite 1 von 18 7–2 7 Typen und Datenkonzepte Eiffel Klassen Jeder Typ ist durch eine Klasse beschrieben. Die Typ-Spezialisierungsstruktur ist typisch für rein objektorientierte Sprachen. Java Klassen sprachdefinierte Klassen, Standardbibliotheksklassen Basisklassen programmiererdefinierte Klassen Java zu ergänzen. C# C# zu ergänzen. 7.4 Sprachdefinierte Typen mit Operatoren Wir geben jeweils untereinander die Typnamen, Beispiele für Konstanten dieser Typen, und dazu definierte Operatoren an. Bei C* nennen wir nur nebeneffektfreie Operatoren. Für alle Typen sind Tests auf Gleichheit und Ungleichheit definiert. Tabelle 7.1 Vordefinierte Typen Component Pascal Eiffel C++ Java C# primitive type simple type Basistypen basic type basic class =, # =, /= fundamental type ==, != boolesche Daten BOOLEAN BOOLEAN FALSE, TRUE False, True ~, &, OR not, and then, or else, xor, implies, and, or bool boolean bool false, true !, &&, || und wie bei Integer !, &&, ||, <, <=, >, >= !, &&, ||, ^, &, | Zeichen CHARACTER SHORTCHAR char kombinierbar mit Qualifikator signed oder unsigned CHAR char wchar_t 'a' "b", 12X, '%/12/', '%N' '\x12', '\n' '\u0012', '\n' <, <=, >, >= - und wie bei Integer © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.4 Sprachdefinierte Typen mit Operatoren Component Pascal 7–3 Eiffel C++ Java C# natürliche Zahlen unsigned char byte unsigned short int ushort unsigned int uint unsigned long int ulong 0xAFFE - 0xAFFE - <, <=, >, >=, +, -, *, /, /, %, !, &&, ||, ?: <, <=, >, >=, +, -, *, /, /, %, !, &&, ||, ?: (logic) (logic) ~, &, |, ^, <<, >> ~, &, |, ^, <<, >> (bit) (bit) ganze Zahlen signed char BYTE byte int kombinierbar SHORTINT INTEGER short mit Qualifikatoren signed, short oder long INTEGER LONGINT int long 123, -456 0xAFFE 0AFFEH 123_456_789 DIV, MOD //, \\, - ^ (power) /, %, !, &&, ||, ?: (logic) ~, &, |, ^, <<, >> (bit) (unsigned right shift) >>> - - Dezimalzahlen - decimal Gleitpunktzahlen SHORTREAL REAL float float 32 Bit IEEE 754 REAL DOUBLE double double 64 Bit IEEE 754 long double 1.0, 1.2E3 1.0F, 1.2E3F 1.0, 1.2E3 4.5D-6 4.5E-6 - 6.7E8L <, <=, >, >=, +, -, *, / 27.9.12 ^ (power) !, &&, ||, ?:, &, |, ^ (logic, bit) © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7–4 7 Typen und Datenkonzepte Component Pascal Eiffel C++ Java C# Mengen SET kein Mengentyp, aber nachbildbar mit Bitoperatoren auf {1, 2, 3} +, -, *, / BIT-Typen Bibliotheksklasse BitSet Integer, generischer Standardbibliotheksklasse - Bitset<n> Abstraktion für halbgeordnete Mengen PART_COMPARABLE, - COMPARABLE Comparable - <, <=, >, >= Abstraktion für algebraische Strukturen - NUMERIC +, - , * , / , ^ Numeric Bits BIT N mit SET realisierbar Bitoperatoren not, and, or, xor, implies, ^, # mit Bitoperatoren auf Integer realisierbar 00100011B Zeiger - POINTER void* - void* Nichtstypen - NONE void Die logischen, Zeichen- und Ganzzahltypen in Component Pascal und Eiffel haben disjunkte Wertebereiche. Dagegen sind die bool-, char- und int-Typen von C++ alle ganzzahlige Typen, sie werden in Ausdrücken ziemlich freizügig hin- und herkonvertiert. Eiffel und C* bieten keinen Mengentyp, ein solcher kann mit einem Bitklassen- bzw. Integertyp und Bitoperatoren simuliert werden. Eiffel COMPARABLE und NUMERIC sind abstrakte Klassen, die der Vererbung dienen. POINTER hat keine Merkmale, die Klasse dient nur zur Beschreibung von Adressen, mit denen ein Eiffel-Programm mit seiner Umgebung, d.h. Nicht-Eiffel-Programmen, kommuniziert. Java Java zu ergänzen. C# C# zu ergänzen. decimal für kommerzielle Anwendungen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.4 Sprachdefinierte Typen mit Operatoren 7–5 Component Pascal, C# BOOLEAN, bool Component Pascal, Java, C# Die Sprachen legen die Wertebereiche der sprachdefinierten Typen fest, sie hängen also nicht von Kompilierern oder Prozessoren ab. Programme sind dadurch leichter portierbar. 27.9.12 haben nur die Werte false und true. Konversion in ganzzahlige Werte ist nicht möglich. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Programmiererdefinierte Typen 7–6 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7.5 Tabelle 7.2 Programmiererdefinierte Typen Art Component Pascal statische Reihung ARRAY Anzahl OF Typ Eiffel Typ[Anzahl] Name ARRAY [Typ] Typ Name [] Typ[] Name char Name [] dynamische Reihung POINTER TO ARRAY OF Typ Zeichenkette ARRAY OF CHAR Bibliotheksklasse STRING RECORD Attributliste END als Klasse mit Zugriffsroutinen realisierbar Zeiger Routine Aufzählung Bibliotheksklasse String Bibliotheksklasse string struct Name { Attributliste } class Name { Elementliste } union Name { Attributliste } mit Vererbung realisierbar class Name { Elementliste } mit Vererbung realisierbar Pointer: Typ * Name POINTER TO Typ PROCEDURE (Formalparameterliste) : Typ C# Typ Name [Anzahl] ARRAY OF Typ varianter Verbund Java generische Bibliotheksklasse offene Reihung Verbund C++ - mit Konstanten realisierbar Reference: Typ & Name class Name { Elementliste } Attribute StructLayout und FieldOffset Pointer: Typ * Name - Typ Name (Formalparameterliste) mit Muster realisierbar enum Name { Symbolliste } 27.9.12 7 Typen und Datenkonzepte enum Name { Symbolliste } struct Name { Attributliste } 7.6 Typdeklarationen, Typdefinitionen 7–7 Eiffel Es gibt nur das Klassenkonstrukt, um Typen zu konstruieren. C++ Die Attribute eines struct sind bitweise implementierbar. Die Attribute einer union werden auf dieselben Speicherzellen gelegt. Eine Referenz entspricht einem konstanten Zeiger und wird implizit dereferenziert. Der void-Typ wird hauptsächlich als Ergebnistyp von Funktionen gebraucht, die Prozeduren sein sollen. Von void gibt es keine Exemplare, da er keine Werte hat. Java Java zu ergänzen. C# C# zu ergänzen. 7.5.1 Operatoren Component Pascal Programmiererdefinierte Typen sind Typen zweiter Klasse: Operatoren sind nämlich für sie nicht durch den Programmierer definierbar. Eiffel, C++ Programmierer- und sprachdefinierte Typen werden ziemlich gleich behandelt. Eiffel In Eiffel sind Operatoren syntaktische Varianten von Funktionen, die in einer Klasse definiert oder redefiniert werden. Für programmiererdefinierte Typen können Operatoren mit beliebigen Sonderzeichen als Namen in Präfix- und Infixschreibweise – wie für sprachdefinierte Typen – definiert werden. Konsistente Semantik von Operatoren wird über Abstraktionen und Vererbungsbeziehungen erreicht. C++ C++ nutzt für programmiererdefinierte Operatoren vor allem den Mechanismus des Überladens. Nur Operatorsymbole, die schon in der Sprache definiert sind, können gewählt werden. Operatoren können global oder an Klassen gebunden sein. Konsistente Semantik von Operatoren ist eher der Disziplin der Programmierer überlassen. Java Java zu ergänzen. C# C# zu ergänzen. 7.6 Typdeklarationen, Typdefinitionen Component Pascal Es gibt anonyme und benannte Typen. Eine Typdeklaration führt einen neuen Typ mit neuem Namen ein und hat die Form TYPE Typname = Typangabe; Typangaben bilden eine syntaktische Einheit. Typ- und Variablennamen werden vorangestellt, durch = bzw. : getrennt. Eiffel Die Namen sprachdefinierter Typen können mit anderer Bedeutung belegt werden. Es gibt keine Typdefinitionen im Sinne von Component Pascal oder C++. Jede Klasse erhält in ihrer Deklaration einen Namen. Typdeklarationen gibt es in folgenden Arten: -- Normalfall Klasse = Typ Klassenname [Klassenname] -- von generischer Klasse abgeleiteter Typ formaler generischer Klassenname -- Parameter einer generischen Klasse like Objektname -- verankerter Typ BIT N -- Bittyp Es gibt anonyme und benannte Typen. Eine Typdefinition gibt einem Typ einen neuen Namen, sie führt aber keinen neuen Typ ein und hat näherungsweise die Form Klassenname C++ typedef Typangabe_Teil_1 Typname Typangabe_Teil_2; Syntaktisch kombinieren Typangaben Elemente von Präfix- und Postfixnotationen. Manche Teile einer Typangabe stehen vor, manche nach einem Typ- oder Variablennamen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7–8 7 Typen und Datenkonzepte Die Namen sprachdefinierter Typen sind reservierte Wortsymbole. Java Java zu ergänzen. C# C# zu ergänzen. 7.7 Typgleichheit, Typäquivalenz Component Pascal, Eiffel Typgleichheit ist im wesentlichen als Namensäquivalenz definiert. C++ Typäquivalenz bedeutet bei structs Namensäquivalenz. Dagegen gelten die von einem Typ Typ abgeleiteten Zeiger-, Referenz-, Reihungs- und Funktionstypen Typ *, Typ &, Typ [], Typ () weniger als eigenständige Typen denn als eben abgeleitete Typen. Typdefinitionen dafür sind zwar möglich, aber eher unüblich, da für sie auch nur strukturelle Äquivalenz, nicht Namensäquivalenz gilt. Java Java zu ergänzen. C# C# zu ergänzen. 7.8 Wertprüfungen zur Laufzeit Bei allen fünf Sprachen ist Unter- und Überlauf von Wertebereichen zur Laufzeit i.A. möglich. Alle Sprachen bis auf C++ prüfen die Einhaltung von Wertebereichen zur Laufzeit, d.h. nur C++ ist in dieser Hinsicht nicht typsicher. 7.9 Implizite Typanpassung Component Pascal Numerische Typen werden in arithmetischen Ausdrücken, bei Zuweisungen und Parameterübergaben implizit vom kleineren zum größeren Typ angepasst, sodass keine Information, höchstens Genauigkeit verlorengeht. Die Kompatibilitätsregeln beruhen auf dem Typeinschluss BYTE ⊆ SHORTINT ⊆ INTEGER ⊆ LONGINT ⊆ SHORTREAL ⊆ REAL. Der Kompilierer prüft Typkompatibilität. Eiffel Numerische Typen werden in arithmetischen Ausdrücken, bei Zuweisungen und Parameterübergaben implizit vom leichteren zum schwereren Typ angepasst, sodass keine Information, höchstens Genauigkeit verlorengeht. Die Konformitätsregeln beruhen auf der Balancierungsregel INTEGER ⊆ REAL ⊆ DOUBLE. Der Kompilierer prüft Typkonformität. C++ Alle Fundamentaltypen können in Ausdrücken, bei Zuweisungen und Parameterübergaben beliebig eingesetzt werden, sie werden implizit in den geforderten Typ ohne Rücksicht auf Verlust an Genauigkeit oder Information konvertiert. Es gibt daher weder Kompatibilitätsprüfungen des Kompilierers noch Laufzeitprüfungen.1 Java, C# Wie bei Component Pascal und Eiffel wird implizit verlustlos vom kleineren zum größeren Typ angepasst. 1 Der Autor fragt sich, ob der Begriff „strongly typed language“ für eine Sprache angemessen ist, die statt statischer Typprüfung Typkonversion einsetzt. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.10 Explizite Typanpassung, Typkonversion 7–9 7.10 Explizite Typanpassung, Typkonversion Component Pascal, Eiffel Es gibt kein Sprachkonstrukt zur Konversion programmiererdefinierter Typen oder Klassen. Der Programmierer kann natürlich selbst Konversionsfunktionen schreiben, wenn solche benötigt werden. Component Pascal Zur expliziten Typanpassung bestimmter Basistypen gibt es die sprachdefinierten Funktionen CHR, ENTIER, LONG, ORD, SHORT. Eiffel Die Bibliotheksklasse BASIC_ROUTINES enthält für Basisklassen die Konversionsfunktionen charcode, charconv, double_to_integer, double_to_real, real_to_integer. C++ Die aus C übernommene Typecast-Notation zur Typkonversion kann auf beliebige Typen, also auch auf Klassen angewandt werden. Außerdem gibt es dafür eine funktionale Notation. Typecast-Notation: Some_type * obj_some = (Some_type*) obj; ((Some_type *) obj)->feature_of_Some_type; Funktionale Notation: typedef Other_type * Other_ptr; Other_ptr obj_other = Other_ptr(obj); Other_ptr(obj)->feature_of_Other_type; Man beachte, dass damit beliebige Konversionen möglich sind, ohne dass ein Typtest durchgeführt wird.1 Wenn das Objekt nicht mit dem Zieltyp verträglich ist, können beliebig seltsame und unerwünschte Effekte auftreten. Leitlinie 7.1 C++: Vermeide Typkonversionen Vermeide Typkonversionen! Oft ist ihre Verwendung Ausdruck eines schwachen Entwurfs oder schlechten Programmierstils. Java Java zu ergänzen. C# C# zu ergänzen. Bibliotheksklasse: System.Convert. Typecasts mit C-Notation: (Typ) Ausdruck 7.11 Statischer und dynamischer Typ Eine Unterscheidung zwischen statischem und dynamischem Typ ist nur in Zusammenhang mit Vererbung relevant. Der statische Typ einer Größe ist der Typ, mit dem sie (zur Kompilationszeit) vereinbart ist. Der dynamische Typ einer Größe ist der Typ ihres Werts zur Laufzeit. Der dynamische Typ entscheidet, welche Version einer gerufenen Operation ausgeführt wird. Component Pascal Bei einer Zeigervariablen oder einem Referenzparameter eines Verbundtyps kann der dynamische Typ eine Erweiterung des statischen Typs sein. Eiffel Bei Größen mit Referenzsemantik kann der dynamische Typ eine Erweiterung des statischen Typs sein. 1 Wir haben es hier mit einer „streng typisierten Sprache“ zu tun, die uns alle Mittel bietet, um sämtliche Typprüfungen zu unterlaufen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7 – 10 C++ 7 Typen und Datenkonzepte Die Terminologie ist anders: „Ein Objekt einer abgeleiteten Klasse kann als Objekt ihrer Basisklasse behandelt werden, wenn es mit einem Zeiger manipuliert wird“ [Str91] S. 184. Statt Zeiger erfüllen auch Referenzen diesen Zweck. Im Unterschied zu Component Pascal und Eiffel besitzt die Größe in C++ nicht einen dynamischen Typ, sondern wird vom statischen Typ in diesen konvertiert. Java Java zu ergänzen. C# C# zu ergänzen. 7.12 Typinformationen zur Laufzeit Component Pascal Es gibt das Konstrukt Typtest. IF obj IS SomeType THEN ... ELSIF obj IS OtherType THEN ... END; Eiffel Viele Probleme lassen sich mit dem Mechanismus verankerter Typen (anchored types) und der Kovarianzregel ohne Typtests lösen. Die gemeinsame Oberklasse ANY und damit jede Klasse enthält das Merkmal generator : STRING das den Klassennamen als Zeichenkette liefert. Das Beispiel if obj.generator.is_equal ("SOME_TYPE") then ... elseif obj.generator.is_equal ("OTHER_TYPE") then ... end ist freilich nicht eleganter als das C++-Beispiel weiter unten. Den Typvergleich zweier Objekte erlaubt das in ANY definierte Merkmal conforms_to (other : like Current) : BOOLEAN bei dem ein verankerter Typ verwendet wird. Anwendungsbeispiel: if obj_some.conforms_to (obj_other) then ... C++ Der Operator typeid, der der Abfrage des dynamischen Typs eines Objekts zur Laufzeit dient, gehört zu den jüngsten Elementen des Sprachstandards. if (typeid(obj) == typeid(SomeType)) { ... } else if (typeid(obj) == typeid(OtherType)) { ... }; Der Klassenname steht auch als Zeichenkette bereit. Das Beispiel if (strcmp(typeid(obj).name(), typeid(SomeType).name())) ... zeigt eine nicht gerade elegante Anwendung. In der Menge der Typen gibt es eine totale Ordnung, bezüglich der zwei Objekte verglichen werden können: if (typeid(obj_some).before(typeid(obj_other))) ... Es ist aber nicht garantiert, dass diese Ordnungsrelation etwas mit der Vererbungsrelation zu tun hat! Der Nutzen dürfte daher begrenzt sein. Java Java zu ergänzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.13 Typprüfungen zur Laufzeit C# 7 – 11 C# zu ergänzen. typeof is 7.13 Typprüfungen zur Laufzeit Component Pascal Es gibt die Konstrukte Typzusicherung und bewachte Anweisung. Typzusicherung: obj(SomeType).featureOfSomeType; Bewachte Anweisung: WITH obj : SomeType DO obj.featureOfSomeType; | obj : OtherType DO obj.featureOfOtherType; ELSE ... END; Eiffel Für die Zuweisung eines Objekts unbekannten Typs gibt es den Zuweisungsversuch (assignment attempt). obj_some : SOME_TYPE obj_other : OTHER_TYPE obj_some ?= obj obj_other ?= obj if obj_some /= Void then obj_some.feature_of_SOME_TYPE elseif obj_other /= Void then obj_other.feature_of_OTHER_TYPE else ... end C++ Statt Typprüfungen gibt es Typkonversionen, siehe 7.14 S. 11. Java Java zu ergänzen. C# C# zu ergänzen. as 7.14 Typkonversionen zur Laufzeit Component Pascal, Eiffel Typkonversionen zur Laufzeit sind nicht möglich (und nicht notwendig). C++ Konstrukte zur Konversion eines Objekts von einem Typ in einen anderen zur Laufzeit wurden spät in den Sprachstandard aufgenommen. Sie beinhalten die Operatoren static_cast reinterpret_cast const_cast dynamic_cast In [Str91] und [ElS90] sind sie auf eine Weise erklärt, die für einen durchschnittlich begabten Menschen des europäischen Kulturkreises nicht ganz leicht zu verstehen ist. Das Problem wird in der Literatur breit diskutiert. Java Java zu ergänzen. C# C# zu ergänzen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7 – 12 7 Typen und Datenkonzepte 7.15 Zeiger Component Pascal Zeiger beziehen sich nur auf dynamische Variable eines Verbundtyps oder eine offene Reihung (die auf der Halde liegen). Referenzierung: TYPE Some = RECORD ... END; VAR ptr : POINTER TO Some; Dereferenzierung: some := ptr^; Es gibt den speziellen Wert NIL für den Bezug auf nichts. Eiffel Es gibt keine spezielle Notation für Zeiger, da sie nicht erforderlich ist. Variablen, Parameter usw. sind i.d.R. Bezüge auf Objekte. Es wird implizit dereferenziert. C++ Zeiger beziehen sich auf statische oder dynamische Variablen beliebigen Typs. Sie können sich auf beliebige Speicheradressen beziehen. Referenzierung: int * ptr, i; Adresszuweisung: ptr = &i; Dereferenzierung: i = *ptr; Als spezieller Wert für den Nichtsbezug dient 0, NULL oder '\0'. Es gibt eine spezielle Zeigerarithmetik (Addition, Subtraktion). Außerdem gibt es Referenzen. Das sind konstante Zeiger, entsprechen also etwa dem Pseudokonstrukt CONSTANT POINTER TO ... Die Adresszuweisung erfolgt im Initialisierungsteil der Definition ohne expliziten Adressoperator: int i = 123; int & ref = i; Die Dereferenzierung erfolgt implizit: ref = 123; Referenzen sind vor allem als Referenzparameter nützlich. Java Java zu ergänzen. C# C# zu ergänzen. 7.16 Reihungen Component Pascal Reihungen werden statisch vereinbart. Die Elementzahl ist durch eine positive ganze Zahl gegeben, der Indexbereich beginnt immer bei 0, der Elementtyp ist beliebig. VAR a : ARRAY 10 OF REAL; Der Wert der Variablen a ist das Feld. Zugriff auf die Elemente mit indizierten Variablen: a [0], a [9] Die Überschreitung von Bereichsgrenzen wird zur Laufzeit überprüft. Mehrdimensionale Reihungen sind möglich. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.16 Reihungen 7 – 13 VAR b : ARRAY 8, 7 OF REAL; b [1][2] und b [1, 2] sind äquivalent. Als formale Parameter sind offene Reihungen (ohne Angabe der Elementzahl) erlaubt. Reihungen können dynamisch erzeugt werden, wenn ein Zeiger auf eine offene Reihung vereinbart ist. VAR openArr : POINTER TO ARRAY OF REAL; ... NEW (openArr, 64); Eiffel Reihungen werden als Objekte einer Bibliotheksklasse dynamisch erzeugt. Der Indexbereich ist durch zwei ganzzahlige Werte festgelegt, der Elementtyp ist beliebig. a : ARRAY [REAL] Der Wert der Variablen a ist ein Bezug auf ein Objekt des Typs ARRAY [REAL], der von der generischen Klasse ARRAY abgeleitet ist. Erzeugung eines Objekts: create a.make (0, 9); Zugriff auf die Elemente erfolgt ausschließlich über Zugriffsroutinen: a.put (0, x) x := a.item (9) Für den Lesezugriff gibt es auch eine Infix-Notation: x := a @ 9 Die Überschreitung von Bereichsgrenzen wird durch Zusicherungen (Vorbedingungen, Invarianten) geprüft. Mehrdimensionale Reihungen sind durch geschachtelte Vereinbarungen b : ARRAY [ARRAY [REAL]] oder spezielle Klassen möglich. C++ Eigentlich gibt es in C/C++ keine Reihungen, sondern nur eine Reihungsschreibweise für Zeiger. Die Elementzahl ist durch eine positive ganze Zahl gegeben, der Indexbereich beginnt immer bei 0, der Elementtyp ist beliebig. float a [10]; Der Wert der Variablen a ist ein Bezug auf das Feld. Zugriff auf die Elemente mit indizierten Variablen oder Zeigerarithmetik: a[0] ist äquivalent mit *a a[9] ist äquivalent mit *(a + 9) Die Überprüfung der Überschreitung von Bereichsgrenzen ist eine seltene Kompiliereroption. Die Bereichsgrenzen sind zudem zur Laufzeit nicht bekannt! Sie müssen daher bei Bedarf in zusätzlichen Variablen mitgeführt werden. Mehrdimensionale Reihungen sind möglich, allerdings muss der Programmierer bei Parameterübergabe die Speicherabbildungsfunktion beherrschen. float b [length * width]; b[1][2] und b[1, 2] sind nicht äquivalent (aber beide legal). Reihungen können dynamisch erzeugt werden. Reihungsschreibweise: 27.9.12 Zeigerschreibweise: © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7 – 14 7 Typen und Datenkonzepte float open_arr []; ... open_arr = new float[64]; float * open_arr; Man beachte, dass dynamisch erzeugte Variable explizit gelöscht werden müssen, da C++ keine automatische Speicherbereinigung bietet. Es empfiehlt sich, in C++ auf die von C übernommenen Reihungen sowie die Zeigerarithmetik weitgehend zu verzichten und stattdessen die generische vector-Bibliotheksklasse der STL zu verwenden, da diese sicherer ist. Java Java zu ergänzen. C# C# zu ergänzen. 7.17 Zeichenketten Component Pascal Zeichenketten sind Reihungen von Zeichen. Sie werden mit dem Zeichen 0X abgeschlossen. VAR s : ARRAY 10 OF CHAR; Es gibt ein Bibliotheksmodul Strings mit Prozeduren zur Zeichenkettenverarbeitung. Eiffel Zeichenketten werden als Objekte einer Bibliotheksklasse dynamisch erzeugt. Die Länge ist durch einen ganzzahligen Wert festgelegt. s : STRING Der Wert der Größe s ist ein Bezug auf ein Objekt der Klasse STRING. Erzeugung eines Objekts: create s.make (10) Die Klasse enthält viele Routinen zur Zeichenkettenverarbeitung. C++ Zeichenketten sind als Zeiger auf Zeichen oder als Reihungen von Zeichen darstellbar. char * s; char t [10]; Eine Zeichenkette wird durch das Zeichen 0 oder '\0' abgeschlossen. Dies erlaubt eine effiziente Speicherung. Die Überschreitung von Bereichsgrenzen wird i.A. nicht geprüft (kein Laufzeitfehler), sondern führt zu fehlerhaften Zugriffen auf fremde Speicherbereiche. Es gibt eine umfangreiche ANSI-C-Bibliothek (string.h) mit Funktionen zur Zeichenkettenverarbeitung sowie eine C++-Bibliotheksklasse string, deren Benutzung vorzuziehen ist. Java Java zu ergänzen. C# C# zu ergänzen. 7.18 Verbunde Component Pascal Die Typen der Attribute sind beliebig. VAR r: RECORD name : ARRAY 20 OF CHAR; age : INTEGER; salary : REAL; END; Zugriff auf die Elemente mit Punktschreibweise: © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.19 Mengen 7 – 15 r.name, r.age, r.salary Verbunde sind nicht (durch den Programmierer) implementierbar. Eiffel Verbunde können als Klassen realisiert werden. Die Attribute sind von außen nicht zugreifbar. Schreibzugriffe müssen mit Zugriffsroutinen realisiert werden. Dies entspricht dem Konzept der Datenabstraktion. C++ Die Typen der Attribute sind beliebig. struct { char int float } r; name [20]; age; salary; Zugriff auf die Elemente mit Punktschreibweise: r.name, r.age, r.salary Verbunde sind implementierbar. struct Some_byte { int bits0to2 : 3; int : 2; int bits5to7 : 3; }; // unused Das Konstrukt ist trotzdem nicht portierbar, da die tatsächliche Implementation vom Kompilierer abhängt. Java Java zu ergänzen. C# C# zu ergänzen. 7.19 Mengen Component Pascal Die Elemente einer Menge sind ganze Zahlen aus einem kleinen Wertebereich (0 .. 31). VAR s : SET; Es gibt Mengenoperatoren und -prozeduren. Eiffel Mengen mit beliebigem Elementtyp werden als Objekte einer generischen Bibliotheksklasse dynamisch erzeugt. Der Elementtyp ist eine beliebige Klasse. s : SET [INTEGER]; Die Klasse enthält viele Routinen, darunter Mengenoperatoren und Zugriffsroutinen auf Elemente. Java Java zu ergänzen. C# C# zu ergänzen. 7.20 Einfache Typen 7.20.1 Zeichen und Zahlen Component Pascal Zeichen und Zahlen sind unterscheidbare Typen mit unterschiedlichen Operatoren. Die Anpassung erfolgt explizit mit Standardfunktionen. VAR c : CHAR; i : INTEGER; c := 'A'; c := CHR (65); i := 65; i := ORD (0AX); i := ORD ('A') + ORD ('B'); 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7 – 16 Eiffel 7 Typen und Datenkonzepte Eiffel bietet konzeptuell dasselbe wie Component Pascal, nur mit den Funktionen charconv und charcode der Klasse BASIC_ROUTINES statt CHR und ORD. C++ Zeichen und Zahlen werden kaum unterschieden. Auf Zeichen sind arithmetische Operationen anwendbar. char c; int i; c = 'A'; c = 65; c = '\65'; i = 65; i = '\xA'; i = 'A' + 'B'; Java Java zu ergänzen. C# C# zu ergänzen. 7.20.2 Boolesche Daten C C kannte lange keinen booleschen Datentyp und keine logischen Ausdrücke in strengem Sinn. Der Wert 0 wurde mit false identifiziert, jeder andere Wert mit true. Dies führte dazu, dass C-Programme mit untereinander inkompatiblen Definitionen der Art #define BOOLEAN unsigned char #define FALSE 0 #define TRUE 1 enum Boolean {False, True}; typedef enum {false = 0, true = 1} BOOL; angefüllt wurden, sowie zu einem heftigen Religionskrieg, ob denn in C++ ein boolescher Datentyp aufgenommen werden sollte oder nicht. C++ Der Friedensvertrag wurde mit dem C++-Standard geschlossen, der mit bool einen ganzzahligen Typ mit den Werten false und true bietet. Es wird freizügig implizit konvertiert, um Kompatibilität mit alten C-Programmen aufrechtzuerhalten: false nach 0 und zurück, true nach 1, jeder von 0 verschiedene Wert von Zahlen oder Zeigern nach true. C99 Schließlich erhielt C99 das Schlüsselwort _Bool, das einen zweiwertigen Zahlentyp mit den Werten false und true bezeichnet. Zudem gibt es die C-Standardschnittstellendatei stdbool.h mit Makros zu den Spezifikationen: bool false true expandiert zu expandiert zu expandiert zu _Bool 0 1 Java Java zu ergänzen. C# C# zu ergänzen. 7.20.3 Aufzählungen Component Pascal Die aus Pascal und Modula-2 bekannten Aufzählungstypen fielen in Component Pascal dem Prinzip der Minimierung der Sprachkonzepte zum Opfer. Aufzählungen müssen mit ganzzahligen Konstanten realisiert werden, wobei Typsicherheit verlorengeht. CONST red green blue numberOfColours = 0; = 1; = 2; = 3; VAR colour : INTEGER; © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 7.21 Konstanten 7 – 17 ASSERT ((0 <= colour) & (colour <= numberOfColours), BEC.invariant); Eiffel red, green, blue : INTEGER is unique colour : INTEGER invariant colour = red or colour = green or colour = blue Die den Bezeichnern red, green, blue zugeordneten Werte sind klassenweit eindeutige konsekutive positive Ganzzahlen. Die Einhaltung des Wertebereichs wird hier mit einer Invariante geprüft. C++ Die Aufzählungstypen sind aus C übernommen. enum Colour {red, green, blue}; Colour colour; Aufzählungen sind mit ganzen Zahlen verträglich, d.h. es wird implizit konvertiert. Ob bei einem Typkonflikt der Kompilierer eine Warnung liefert? Ein Aufzählungstyp ist implementierbar. enum Colour {red = 0, green = 9, blue = -1}; Java Java zu ergänzen. public class Colour { public static final red = 0; public static final green = 1; public static final blue = 2; } public class ColourClient { int colour = Colour.red; } C# C# zu ergänzen. 7.21 Konstanten Component Pascal Beispiele für literale Konstanten: 'a', 1, 2.3, 4.5E-6, 7E8, 'cde', "f", "ghe" Konstantennamen für einfache Werte sind in einer Konstantenvereinbarung definierbar. Es handelt sich um Übersetzungszeitkonstanten. CONST pi = 3.14; Es gibt Hexadezimalkonstanten. 0AFFEH Eiffel Beispiele für literale Konstanten: "a", 1, 2.3, 4.5E-6, 7E8, 9.E2, .3E-4, "cde" Namen für Werte von Basistypen sind in einer Merkmalsvereinbarung einer Klasse definierbar. pi : REAL = 3.14; In diesem Beispiel ist eine Einmalfunktion günstig einsetzbar. pi : REAL once Algorithmus zur Berechnung von pi Result := Ausdruck end Kommt es auf den Wert nicht an, kann das Wortsymbol unique verwendet werden. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 7 – 18 7 Typen und Datenkonzepte left, right : INTEGER is unique; Einmalfunktionen können auch für konstante Objekte und zur Bildung von Laufzeitkonstanten verwendet werden. C++ Jede literale Zahlenkonstante in Component Pascal ist auch eine in C++, aber nicht umgekehrt. Beispiele für literale Konstanten: 'a', 1, 2.3, 4.5E-6, 7E8, 9.E2, .3E-4, "cde" Oktalzahlen: 012 Hexadezimalzahlen: 0xAFFE Der Typ kann durch Anhängen von U, L oder F festgelegt werden (kleine e, u, l, f tun’s auch). 0x123U, 456L, 7.8F, 9.1E2L C kennt keine Konstantennamen. Solche sind aber mit einer Vorübersetzerdirektive definierbar. Sie werden vor der Kompilation substituiert. #define pi (3.14) C++ bietet aber richtige Konstanten, deren Benutzung man vorziehen sollte. const float pi = 3.14; Java Java zu ergänzen. public static final float pi = 3.14; C# C# zu ergänzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8 Ablaufsteuerung und algorithmische Konzepte Aufgabe 8 8.1 Routinen Unter Routinen verstehen wir Prozeduren und Funktionen. Die syntaktischen Einheiten zur Vereinbarung von Routinen sind in jeder Sprache anders bezeichnet. Tabelle 8.1 Routinen Component Pascal Eiffel Routine Prozedur Routine Funktion Prozedur gewöhnliche Prozedur Kommando void-Funktion Funktion Funktion, Funktionsprozedur Funktion nicht-void-Funktion 8.1.1 Prozeduren Component Pascal Prozedurdeklaration: C++ Java C# PROCEDURE Prozedurname (Formalparameterliste); lokale Vereinbarungen BEGIN Anweisungen END Prozedurname; Der Prozeduraufruf ist eine Anweisung: Prozedurname (Aktualparameterliste); Eiffel Spezielle Merkmalsdeklaration: Prozedurname (Formalparameterliste) -- Kurzbeschreibung require Vorbedingungen local lokale Vereinbarungen do Anweisungen ensure Nachbedingungen end Zur Spezifikation von Routinen mit Vor- und Nachbedingungen sind spezielle (optionale) Zusicherungsabschnitte eingebaut. Der spezielle Merkmalsaufruf ist eine Anweisung: Prozedurname (Aktualparameterliste) C++, Java, C# Funktionsdefinition: void Prozedurname (Formalparameterliste) { lokale Vereinbarungen Anweisungen }; Eine Prozedur ist eine Funktion, deren Ergebnis vom „Nichtstyp“ void ist. Man beachte, dass in C++ p (...) nicht eine Prozedur, sondern äquivalent zu int p (...), also eine Funktion mit Ergebnistyp int ist. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 8 – Seite 1 von 20 8–2 8 Ablaufsteuerung und algorithmische Konzepte Vereinbarungen sind spezielle Anweisungen und treten daher innerhalb von Verbundanweisungen auf. Der Funktionsaufruf ist ein Ausdruck, der als Ausdrucksanweisung hingeschrieben wird: Prozedurname (Aktualparameterliste); 8.1.2 Funktionen Component Pascal Funktionsdeklaration: PROCEDURE Funktionsname (Formalparameterliste) : Ergebnistyp; lokale Vereinbarungen BEGIN Anweisungen RETURN Ausdruck vom Ergebnistyp; END Funktionsname; Der Funktionsaufruf ist ein Ausdruck Funktionsname (Aktualparameterliste) dessen Wert weiter verarbeitet werden muss. Er kann syntaktisch keine Anweisung sein. Eiffel Spezielle Merkmalsdeklaration: Funktionsname (Formalparameterliste) : Ergebnistyp -- Kurzbeschreibung require Vorbedingungen local lokale Vereinbarungen do Anweisungen Result := Ausdruck vom Ergebnistyp ensure Nachbedingungen end Result ist eine implizit vereinbarte lokale Größe vom Ergebnistyp. Das Ergebnis muss einen Namen haben, damit man es in Nachbedingungen verwenden kann. Der spezielle Merkmalsaufruf ist ein Ausdruck Funktionsname (Aktualparameterliste) dessen Wert weiter verarbeitet werden muss. Er kann syntaktisch keine Anweisung sein. C++, Java, C# Funktionsdefinition: Ergebnistyp Funktionsname (Formalparameterliste) { lokale Vereinbarungen Anweisungen return Ausdruck vom Ergebnistyp; }; Der Funktionsaufruf ist ein Ausdruck Funktionsname (Aktualparameterliste) dessen Wert weiter verarbeitet werden muss. Als Ausdrucksanweisung hingeschrieben ist er nur sinnvoll (aber problematisch), wenn die Funktion einen Nebeneffekt produziert: Funktionsname (Aktualparameterliste); © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 Parameter- und Ergebnisübergabe Tabelle 8.2 Parameter- und Ergebnisübergabe Eigenschaft Component Pascal Formalparameterliste Eiffel C++ Java par1 : Type1; par2 : Type2 Type1 par1, Type2 par2 par1, par2 : Type - Abkürzung bei gleicher Art und gleichem Typ C# 8.1 Routinen 27.9.12 8.1.3 Leeres Klammernpaar () bei Vereinbarung und Aufruf einer parameterlosen Routine erforderlich? Prozedur ohne Funktion mit Referenzübergabe mit Type par_val parVal : Type ja Eingabereferenz IN parRef : StructuredType Ausgabereferenz OUT parRef : Type Ein-/Ausgabe-Referenz VAR parRef : Type kein Konstrukt vorhanden, aber übergebene Größen sind meist Referenzobjekte const Type & par_ref const Type * par_ptr ja übergebene Größen sind meist Referenzobjekte möglich durch Wertübergabe - einer Referenz: Type & par_ref oder eines Zeigers: Type * par_ptr Ergebnisübergabe mit spezieller Sprunganweisung RETURN mit implizit definierter Größe Result Zuweisung an Formalparameter erlaubt (außer bei IN) verboten out Type parRef ref Type parRef mit spezieller Sprunganweisung return erlaubt (außer bei const) erlaubt 8–3 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Wertübergabe ohne 8–4 8 Ablaufsteuerung und algorithmische Konzepte C++ f () stellt in C++ eine parameterlose Funktion dar, in C dagegen eine Funktion mit beliebig vielen Argumenten beliebigen Typs. Eine parameterlose Funktion muss in C als f (void) vereinbart werden. Java Java zu ergänzen. C# C# zu ergänzen. Variable Parameteranzahl. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8.1 Routinen 8.1.4 8–5 Eigenschaften von Routinen Tabelle 8.3 Eigenschaften von Routinen Eigenschaft Component Pascal Eiffel Routinenaufruf Prozeduraufruf ist C++ Java C# Routinenname (Aktualparameterliste) Anweisung (statement) Anweisung (instruction) Ausdruck (expression) Funktionsaufruf ist während Ausführung der Routine Existenzdauer lokaler Größen falls mit static spezifiziert: über Aufrufe hinweg - von innen nach außen lokale Größen verdecken globale Größen Merkmale und Parameter dürfen nicht gleiche Namen haben Sichtbarkeitsregeln - Aliasing mit globalen Größen und Formalparametern möglich möglich Nebeneffekte bei Funktionen vermeidbar Routinentypen ja Schachtelung ja kaum vermeidbar nein Maßnahme bei indirekter Rekursion ja nein Rekursion Vereinbarung vor Aufruf erforderlich - ja ja Vorwärtsdeklaration PROCEDURE R^ (Par...) : Typ; nein ja - Funktionsdeklaration Typ r (Typen); C++ In C++ ist der Aufruf einer nicht deklarierten Funktion verboten, während er in C erlaubt ist. Damit wurde eine Fehlerquelle von C – fehlende Typprüfung – in C++ beseitigt. Java Java zu ergänzen. C# C# zu ergänzen. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8–6 8.1.5 Component Pascal 8 Ablaufsteuerung und algorithmische Konzepte Routinentypen und -variablen TYPE FT = PROCEDURE (x : REAL) : REAL; VAR F : FT; PROCEDURE Sqr (x : REAL) : REAL; BEGIN RETURN x * x; END Sqr; ... F := Sqr; F (1.23); Eiffel Auf Routinentypen wird verzichtet, weil jeder Typ auf einer Klasse beruht, Routinen zu Klassen gehören und die Zwecke, denen Routinentypen in prozeduralen Programmiersprachen dienen, in der objektorientierten Programmierung mit Klassen erfüllbar sind. deferred class FT feature f (x : REAL) : REAL deferred end end -- class FT class SQR inherit FT redefine f end feature f (x : REAL) : REAL do Result := x * x end end -- class SQR ... f : FT sqr : SQR ... C++ Mit eigenem sqr-Attribut: Ohne eigenes sqr-Attribut: create sqr; f := sqr; f.f (1.23); create {SQR} f; f.f (4.56); Mit Typdefinition: Ohne Typdefinition: typedef float (*FT) (float); FT f; float (*f) (float); float sqr (float x) { return x * x; }; ... f = &sqr; (*f)(1.23); f(4.56); // explizite Referenzierung // explizite Dereferenzierung // implizite Dereferenzierung © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8.1 Routinen Java 8–7 public abstract class FT { public abstract float f (float x); } public class Sqr extends FT { public float f (float x) {; return x * x; } } Mit eigenem Sqr-Attribut: Ohne eigenes Sqr-Attribut: Sqr sqr = new Sqr(); FT f = sqr; f.f (1.23); FT f = new Sqr(); f.f (4.56); C# C# zu ergänzen. 8.1.6 Was entspricht den vordeklarierten Component-Pascal-Prozeduren? Abkürzungen: B. B.K. F. P. 27.9.12 Bibliothek Bibliotheksklasse Funktion Prozedur © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Component Pascal Eiffel ASSERT (x : BOOLEAN) Anweisung ASSERT (x : BOOLEAN, n : Integerkonstante) check Etikett : boolescher Ausdruck end DEC (v : Integer) DEC (v, n : Integer) INC (v, n : Integer) NEW (v : Zeiger auf Verbund oder festes Array) NEW (v : Zeiger auf offenes Array; x0,...,xn : Integer) assert boolescher Ausdruck : "Text"; - B. assert.h, F.n void assert(int) void Assert (Ausdruck, Ausnahme) v := v - 1 --v; v-- v := v - n v -= n kein Mengentyp, aber nachbildbar mit BIT-Typen mit Operatoren not, and, or, xor, implies, ^, # B.K. EXCEPTIONS, P. raise (STRING) Bit-Operatoren ~, &, |, ^, <<, >> auf Integer B. stdlib.h, F.n void exit (int), void abort () Anweisung Ausdruck v := v + 1 ++v; v++ v := v + n v += n Erzeugungsanweisung Erzeugungsoperator Erzeugungsoperator v : Typ create v.Prozedur (...) Typ * v = new Typ; Typ v = new Typ(...); Typ * v = new Typ [len]; Typ[] v = new Typ(...)[len]; 27.9.12 8 Ablaufsteuerung und algorithmische Konzepte INC (v : Integer) C# Ausdruck B.K. ANY, P. die (INTEGER) HALT (n : Integerkonstante) Java Anweisung EXCL (v : SET; x : Integer) INCL (v : SET; x : Integer) C++ 8–8 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Tabelle 8.4 Was entspricht den vordeklarierten Component-Pascal-Prozeduren? Was entspricht den vordeklarierten Component-Pascal-Funktionen? Tabelle 8.5 Was entspricht den vordeklarierten Component-Pascal-Funktionen? Component Pascal Eiffel C++ Java C# 8.1 Routinen 27.9.12 8.1.7 B.n stdlib.h, math.h, F.n ABS (x : Numeric) : Typ von x B.K. BASIC_ROUTINES, F. int abs (int) abs (INTEGER) : INTEGER long labs (long) double fabs (double) Schiebeoperatoren ASH (x, n : Integer) : LONGINT x : BIT N; n : INTEGER x^n Integer x; Unsigned n x << n; x >> n CAP (c : CHAR) : CHAR B.K. STRING, P. to_upper B. ctype.h, F. int toupper (int) B.K. BASIC_ROUTINES, F. CHR (x : Integer) : CHAR charconv (INTEGER) : CHARACTER B.K. BASIC_ROUTINES, F.n ENTIER (x : Real) : LONGINT real_to_integer (REAL) : INTEGER double_to_integer (DOUBLE) : INTEGER LEN (a : Array) : LONGINT B.K. ARRAY [Typ], F.n LEN (a : Array; n : Integerkonstante.) : LONGINT capacity, count, lower, upper : INTEGER implizite Konversion, Zeichen sind Zahlen implizite Konversion B. math.h, F. double floor (double) Länge einer Reihung zur Laufzeit nicht abrufbar LONG (x : BYTE) : SHORTINT LONG (x : SHORTINT) : INTEGER LONG (x : INTEGER) : LONGINT LONG (x : SHORTREAL) : REAL implizite Konversion implizite oder explizite Konversion mit Typecast System.Convert 8–9 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Schiebeoperator Eiffel C++ MAX (Grundtyp) : Grundtyp - B. limits.h, Makros Java C# 8 – 10 CHAR_MAX, SCHAR_MAX, UCHAR_MAX, SHRT_MAX, USHRT_MAX, INT_MAX, UINT_MAX, LONG_MAX, ULONG_MAX, FLT_MAX, DBL_MAX, LDBL_MAX MAX (SET): INTEGER - - MIN (Grundtyp) : Grundtyp - B. limits.h, Makros CHAR_MIN, SCHAR_MIN, UCHAR_MIN, SHRT_MIN, USHRT_MIN, INT_MIN, UINT_MIN, LONG_MIN, ULONG_MIN, FLT_MIN, DBL_MIN, LDBL_MIN MIN (SET): INTEGER ODD (x : Integer) : BOOLEAN - - Ausdruck Ausdruck x \\ 2 /= 0 x % 2 != 0 B.K. BASIC_ROUTINES, F. ORD (c : CHAR) : INTEGER SHORT (x :SHORTINT) : BYTE SHORT (x :INTEGER) : SHORTINT SHORT (x : LONGINT) : INTEGER SHORT (x : REAL) : SHORTREAL charcode (CHARACTER) : INTEGER implizite Konversion B.K. BASIC_ROUTINES, F.n double_to_real (DOUBLE) : REAL implizite oder explizite Konversion mit Typecast System.Convert 27.9.12 8 Ablaufsteuerung und algorithmische Konzepte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Component Pascal SIZE (Typ) : Integer Eiffel C++ B.K. INTERNAL, F. Operator physical_size (ANY) : INTEGER Integer sizeof (Typ) Integer sizeof Ausdruck Java - C# Operator Integer sizeof (Typ) 8.1 Routinen 27.9.12 Component Pascal 8 – 11 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8 – 12 8.2 8 Ablaufsteuerung und algorithmische Konzepte Zusicherungen Als Beispiel für die Anwendung von Zusicherungen siehe 2.3 S. 2-26. Zusicherungen können z.B. dazu verwendet werden, die Einhaltung von Wertebereichen zur Laufzeit zu überprüfen. Component Pascal Es gibt allgemeine Zusicherungen in Form vordeklarierter Prozeduren. ASSERT (Bedingung); ASSERT (Bedingung, Fehlernummer); Ist die Bedingung falsch, so wird der Programmablauf mit einem Trap abgebrochen. Zur Information werden die Fehlernummer, der Aufrufkeller mit allen Variablen und die Abbruchstelle im Quellprogramm angezeigt. Die Prüfung der Zusicherungen ist obligatorisch. Eiffel Ein wichtiges Entwurfsziel ist, dass sich die Sprache als Spezifikationssprache eignen und die Entwicklung korrekter Programme unterstützen soll. Den Sprachkonstrukten liegt das Kunden-Lieferanten-Modell (client-supplier-model) zugrunde, auf dem auch eine Entwicklungsmethode aufbaut. Dabei werden Prozeduren mit Vor- und Nachbedingungen, Klassen mit Invarianten spezifiziert. Entsprechend bietet Eiffel folgende Arten von Zusicherungen: Allgemeine Zusicherung als Anweisung: check Bedingung end; Vor- und Nachbedingungen von Routinen als Abschnitte (siehe 8.1.1 S. 1): require Vorbedingungen ensure Nachbedingungen Diese Abschnitte werden vererbt, d.h. eine Redefinition einer Routine muss sich an die geerbte Spezifikation halten. Sie darf nur die Vorbedingungen abschwächen und die Nachbedingungen verstärken. Invarianten von Klassen als Abschnitt: invariant Konsistenzbedingungen Die Konsistenzbedingungen müssen vor und nach jedem Aufruf einer Routine gelten. Der Abschnitt wird vererbt, d.h. eine Unterklasse einer Klasse muss sich an die geerbte Spezifikation halten. Sie darf die Invarianten nur verstärken. Invarianten und Varianten von Schleifen als Abschnitte: invariant Bedingung variant nichtnegativ-ganzzahliger Ausdruck Die require-, ensure- und invariant-Abschnitte sind in objektorientierte Konzepte integriert. Ist eine Prüfbedingung falsch, so wird eine sprachdefinierte Ausnahme (exception) ausgelöst. Diese Ausnahme kann an definierter Stelle auf programmiererdefinierte Weise behandelt werden. Ist keine Ausnahmebehandlung programmiert, so wird der Programmablauf abgebrochen. Die Überprüfung von Zusicherungen ist selektiv zur Laufzeit steuerbar. C++ Zusicherungen sind durch eine Bibliothek mit der Schnittstellendatei assert.h unterstützt. Dort findet sich von ANSI-C das Vorübersetzermakro assert(Bedingung); Ist die Prüfbedingung falsch, so wird der Programmablauf durch Aufruf der Funktion abort () abgebrochen. Zur Information werden der Wert der Bedingung (die ein beliebiger Ausdruck ist), der Name der Quelldatei und die Zeilennummer der Abbruchstelle ausgegeben. Die Vorübersetzerdirektive © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8.2 Zusicherungen 8 – 13 #define NDEBUG schaltet die Überprüfung der Zusicherungen ab (vor der Kompilation). C++ bietet zusätzlich die generische Funktion Assert (Bedingung, Ausnahme); Ist die Prüfbedingung falsch, so wird die übergebene programmiererdefinierte Ausnahme (exception) ausgelöst. Die Reaktion hängt dann von der Ausnahme und ihrer Behandlung ab. Die Überprüfung dieser Zusicherungen ist auch mit NDEBUG steuerbar. Mit Hilfe von Ausnahmen kann man auch eigene Prüffunktionen programmieren. Java Java bietet Zusicherungen erst seit Release 1.4 in Form eines Sprachkonstrukts.1 Zusicherungen werden mit dem Schlüsselwort assert eingeleitet, dem booleschen Ausdruck kann eine Zeichenkette folgen: assert Bedingung : Zeichenkette; Beim Aufruf des Java-Kompilierers ist anzugeben, dass er das Sprachkonstrukt akzeptieren soll: javac -source 1.4 MyClass.java Beim Aufruf des Interpretierers, der Java Virtual Machine, ist anzugeben, ob die Zusicherungen zur Laufzeit geprüft werden soll: java -enableassertions MyClass java -ea MyClass oder nicht: java -disableassertions MyClass java -da MyClass Die Standardeinstellung ist, dass die Zusicherungen nicht geprüft werden. Eine verletzte Zusicherung führt zu einem Abbruch des Programmablaufs mit einer Fehlermeldung der Art: Exception in thread "main" java.lang.AssertionError: Zeichenkette at MyClass.main(MyClass.java:5) C# C# zu ergänzen. 1 http://java.sun.com/developer/technicalArticles/JavaLP/assertions/ (Zugriff 2008-11-24), http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html (Zugriff 2008-11-24). 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 Anweisungen 8.3.1 Übersicht 8 – 14 Tabelle 8.6 Anweisungen Component Pascal Eiffel C++ Java C# Bezeichner = Ausdruck Bezeichner := Ausdruck Prozedurbezeichner (Aktualparameterliste) Blockanweisung: {...} IF ... THEN ... ELSIF ... THEN ... ELSE ... END if ... then ... elseif ... then . . . else ... end CASE ... OF | ... : ... | ... : ... ELSE ... END inspect ... when ... then ... when ... then ... else ... end WITH ... DO ... | ... DO ... ELSE ... END WHILE ... DO ... END REPEAT ... UNTIL ... FOR ... TO ... BY ... DO ... END LOOP ... END EXIT RETURN ... - if (...) ... else ... switch (...) case . . . : . . . break; case . . . : . . . break; default : ... Siehe 7.12 S. 7-10 while (...) ... from ... invariant ... variant ... until ... loop ... end do ... while (...); for (. . .; . . .; . . .) ... - break; return ...; 27.9.12 8 Ablaufsteuerung und algorithmische Konzepte © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8.3 8.3 Anweisungen 8.3.2 8 – 15 Syntaktischer Zucker Ein Programmkonstrukt weist eine Unstetigkeitsstelle auf, wenn zwei Programme, die dieses Konstrukt verwenden und sich in nur einem Zeichen unterscheiden, verschiedene Semantik haben. Component Pascal, Eiffel Alle Steuerkonstrukte benutzen Wortsymbole, um Anweisungsfolgen zu klammern. Deshalb ist keine Verbundanweisung erforderlich.1 Das schließende END bzw. end beseitigt Unstetigkeitsstellen und Zweideutigkeiten älterer Sprachen. In Component Pascal ist das Semikolon ; Endezeichen bei Vereinbarungen und Trennzeichen zwischen Anweisungen. In Eiffel ist das Semikolon bei Vereinbarungen und Anweisungen ein optionales Trennzeichen, d.h. es kann auch weggelassen werden, weil die Syntax so definiert ist, dass es auf ein Semikolon mehr oder weniger nicht ankommt. Der empfohlene Stil ist jedoch, trennende Semikola zwecks besserer Lesbarkeit hinzuschreiben (?). C++ Steuerkonstrukte erwarten immer eine einzelne Anweisung und enden meist ohne Wortsymbol. Deshalb ist eine Verbundanweisung erforderlich, sie verwendet die geschweiften Klammern: { Anweisungen } Manche Konstrukte haben Unstetigkeitsstellen. Beispiel: while (i < 9) ; i++; while (i < 9) i++; Manche Konstrukte liefern Zweideutigkeiten, die durch zusätzliche Sprachregeln aufgelöst werden müssen. Beispiel: if (i < 9) if (k > 1) i = k--; else i = k++; Das Semikolon ist bei manchen Anweisungen ein Endezeichen (Deklaration, Ausdruck, do, break, return), bei anderen nicht. Ein defensiver Schreibstil ist, das Semikolon als Endezeichen bei jeder Anweisung zu setzen. Java Java zu ergänzen. C# C# zu ergänzen. 8.3.3 Semantisches Salz Solches soll man bekanntermaßen nicht in offene Wunden schütten. Component Pascal, Eiffel Vereinbarungen (declaration), Anweisungen (statement, instruction) und Ausdrücke (expression) sind verschiedene syntaktische Einheiten. Zuweisungen sind spezielle Anweisungen. Ausdrücke treten nur als Bestandteile von Anweisungen auf. Bedingungen in Steuerkonstrukten sind stets boolesche Ausdrücke. C++ Eine Vereinbarung (declaration) ist eine spezielle Anweisung (statement). Ein Ausdruck (expression) ist eine spezielle Anweisung. Eine Zuweisung ist ein spezieller Ausdruck. Ausdrücke treten als Bestandteile von Vereinbarungen und Anweisungen auf. C und C++ sind ausdrucksorientierte Sprachen, da sie nicht zwischen Ausdruck und Anweisung unterscheiden. Die Ausführung einer Ausdrucks-Anweisung macht nur Sinn, wenn der Ausdruck bei seiner Auswertung einen Nebeneffekt bewirkt. Der Wert des Ausdrucks kann dabei irrelevant sein. Solche Sprachen beruhen also auf dem Konzept Nebeneffekte bewirkender Ausdrücke. In der Tat gibt es in C/C++ viele Operato- 1 27.9.12 Pascal benötigte sie noch in Form von begin ... end. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8 – 16 8 Ablaufsteuerung und algorithmische Konzepte ren, die Nebeneffekte bewirken, und oft werden Funktionen mit Nebeneffekten programmiert.1 Eine Bedingung in einem Steuerkonstrukt ist ein beliebiger Ausdruck, der implizit zu einem booleschen Wert konvertiert wird. Java Java zu ergänzen. C# C# zu ergänzen. 8.3.4 Zuweisungen C++ Man beachte, dass C/C++ als Zuweisungsoperator = und als Gleichheitsoperator == verwenden.2 Die Ausdrucksorientierung stellt uns eine Falle für Tippfehler, die der Kompilierer nicht findet, da sowohl a = b als auch a == b Ausdrücke sind. Der Ausdruck a=b liefert als Wert den Wert von b, als Nebeneffekt wird dieser Wert der Variablen a zugewiesen. Der Ausdruck a=b=c wird von rechts nach links ausgewertet, also wie a = (b = c) Java Java zu ergänzen. C# C# zu ergänzen. 8.3.5 Auswahlanweisungen 8.3.5.1 Alternative Bedingungen C++ Die if-Anweisung hat keinen elsif-Zweig, dieser kann aber durch geschickte Klammerung simuliert werden. 1 Das Konzept ausdrucksorientierter Sprachen wurde erstmals 1966 von Niklaus Wirth vorgeschlagen und in seiner Programmiersprache Euler realisiert. Über Algol 68 fand es Eingang in C – interessanterweise zu einem Zeitpunkt, als Wirth selbst dieses Konzept bereits verworfen hatte und in Pascal das Konzept der Trennung von Ausdruck und Anweisung realisierte. Aus meiner Sicht ist ein problematischer Aspekt von C/C++, dass sie auf Nebeneffekten beruhen, weil dieses Konzept einer systematischen Entwicklung beweisbar korrekter Programme wie auch der Lesbarkeit und Verständlichkeit von Programmen entgegenwirken kann. Mit Nebeneffekten vernünftig programmieren ist zwar möglich, fordert aber Einsicht und Disziplin. 2 In der Mathematik wird seit rund 350 Jahren weltweit einheitlich das Zeichen „=“ als Gleichheitszeichen benutzt. Die meisten Programmiersprachen haben „=“ für denselben Zweck übernommen und „:=“ als Zuweisungszeichen eingeführt. Wenn C/C++ dafür „==“ und „=“ verwenden, so ist das zwar einerseits nur ein syntaktischer Unterschied, andererseits aber ein nicht gerade geniales Abweichen von einem Standard. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8.3 Anweisungen 8 – 17 if (Ausdruck) { Anweisungen } else if (Ausdruck) { Anweisungen } else { Anweisungen }; Java Java zu ergänzen. C# C# zu ergänzen. 8.3.5.2 Alternative Werte Wir betrachten jeweils dasselbe Beispiel. Component Pascal CASE thisMonth OF | january .. june : StudyHardAtCollege; | july, august : EnjoyHolidays; | september : LookForAJob; ELSE GoToWork; END; Eiffel inspect this_month when january .. june then study_hard_at_college when july, august then enjoy_holidays when september then look_for_a_job else go_to_work end C++ switch (this_month) { case january : case february : case march : case april : case may : case june : study_hard_at_college (); break; case july : case august : enjoy_holidays (); break; case september : look_for_a_job (); break; default : go_to_work (); }; 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8 – 18 8 Ablaufsteuerung und algorithmische Konzepte Ein case dient nur als Einsprungmarke, von da an werden alle folgenden Anweisungen ausgeführt bis zur ersten break-Anweisung; diese führt zu einem Sprung ans Ende der switch-Anweisung. Vergessene break-Anweisungen sind eine Fehlerquelle. Es gibt keine Listen und Intervalle von Fällen, diese können durch mehrere case ohne abschließendes break simuliert werden. Java Java zu ergänzen. C# C# zu ergänzen. 8.3.6 Wiederholungsanweisungen Wir betrachten nur ein Beispiel – die Summierung der ersten positiven geraden Zahlen bis 10 – in verschiedenen Formen: als kopf- und fußgesteuerte Bedingungsschleife, als Zählschleife und als bedingungslose Schleife mit einer Sprunganweisung. Component Pascal VAR s, i : INTEGER; (* Vereinbart für alle Schleifenarten *) s := 0; i := 2; WHILE i <= 10 DO INC (s, i); INC (i, 2); END; s := 0; i := 2; REPEAT INC (s, i); INC (i, 2); UNTIL i > 10; s := 0; FOR i := 2 TO 10 BY 2 DO INC (s, i); END; s := 0; i := 2; LOOP INC (s, i); IF i >= 10 THEN EXIT; ELSE INC (i, 2); END; END; Eiffel s, i : INTEGER from s := 0 i := 2 invariant s >= 0; i >= 2 variant 10 - i until i > 10 loop s := s + i i := i + 2 end © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 8.4 Testhilfen 8 – 19 Die Initialisierung der Schleifenvariablen sowie die Zusicherungen für Schleifeninvariante und -variante sind integriert. Die Bedingung ist eine Abbruchbedingung am Kopf der Schleife. C++ unsigned int s = 0, i = 2; // Vereinbart und initialisiert für alle Schleifenarten // ausgenommen for while (i <= 10) { s += i; i += 2; } do { s += i; i += 2; } while (i <= 10); for (unsigned int k = 2; k <= 10; k += 2) { s += k; } while (true) { s += i; if (i >= 10) { break; } else { i += 2; }; Man beachte, dass bei der fußgesteuerten do-while-Schleife eine Fortsetzungsbedingung steht! Die for-Schleife ist keine eigentliche Zählschleife, sondern eine while-Schleife, bei der (1) (2) (3) eine lokale Laufvariable deklariert und initialisiert, eine Fortsetzungsbedingung und eine Rumpf-Ende-Aktion angegeben werden kann. Java Java zu ergänzen. C# C# zu ergänzen. 8.4 Testhilfen Zum Testen kann es nützlich sein, zusätzliche Anweisungen in ein Programm einzufügen. Nach dem Test sollten sie vor einem erneuten Kompilationsvorgang ausgeblendet werden, damit sie keinen Laufzeitaufwand verursachen. Entfernen oder Auskommentieren von Testanweisungen ist ungeschickt, aufwändig und fehleranfällig. Component Pascal Zum Ein- und Ausblenden von Testanweisungen können Falter (folder) verwendet werden. Sie lassen sich benennen (z.B. als GraphicsTest) und mit einem Befehl selektiv für spezifizierte Module ein- und ausklappen. Eiffel Zum Ein- und Ausblenden von Testanweisungen wird bedingte Kompilation verwendet. Es gibt dazu eine spezielle Anweisung der Form debug ("Graphics_Test") Testanweisungen end Per Kompiliereroption werden debug-Anweisungen nicht oder selektiv für spezifizierte Klassen, Cluster und Schlüssel kompiliert. 27.9.12 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 8 – 20 C++ 8 Ablaufsteuerung und algorithmische Konzepte Zum Ein- und Ausblenden von Testanweisungen wird üblicherweise bedingte Kompilation verwendet. Diese Möglichkeit bietet der Vorübersetzer mit Direktiven der Form #define GRAPHICS_TEST 1 #ifdef GRAPHICS_TEST Testanweisungen #endif Java Java zu ergänzen. C# C# zu ergänzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 9 Syntaktische und lexikalische Aspekte Aufgabe 9 9.1 Kommentare Component Pascal Kommentare werden mit (* und *) geklammert. Sie können geschachtelt werden. Eiffel Es gibt nur Zeilenendkommentare. Sie werden mit -- eingeleitet und erstrecken sich bis zum Zeilenende. C++ Kommentare werden mit /* und */ geklammert oder als Zeilenendkommentare mit // eingeleitet, sie erstrecken sich dann bis zum Zeilenende. Es gibt keine geschachtelten Kommentare. Java Wie bei C++; zusätzlich werden mit /** und */ geklammerte Kommentare vom Werkzeug Javadoc in HTML-Dokumentationsdateien extrahiert. C# Wie bei C++; zusätzlich werden mit /// eingeleitete Kommentare von einem .NETWerkzeug in XML-Dokumentationsdateien extrahiert. 9.2 Zeichencodes Component Pascal, Java, C# Die Sprachdefinitionen nehmen Bezug auf den Unicode. Eiffel Die Sprachdefinition nimmt Bezug auf den ASCII-Zeichencode. C++ Die Sprachdefinition bezieht sich auf den ASCII-Zeichencode. Die Sprache ist jedoch nicht auf diesen festgelegt, da Vorkehrungen getroffen sind, um sie mit anderen Zeichencodes zu realisieren. 9.3 Bezeichner Component Pascal Frei wählbare Bezeichner bestehen aus Latin1-alphanumerischen Zeichen und dem Unterstrich. Das erste Zeichen ist nicht numerisch. Klein- und Großschreibung wird unterschieden. Der Unterstrich dient gemäß Konvention nur der Kompatibilität mit anderen Sprachen; üblich ist die von Smalltalk stammende Groß-Kleinschreibung. Jedes Modul hat seinen eigenen Namensraum. Ein Bezeichner kann in einem Modul mehrfach für verschiedene Größen verwendet werden. Namenskonflikte werden durch Sichtbarkeitsregeln gelöst. Eiffel Frei wählbare Bezeichner bestehen aus alphanumerischen Zeichen und dem Unterstrich. Das erste Zeichen ist alphabetisch. Klein- und Großschreibung wird nicht unterschieden. Es gibt getrennte Namensräume für Klassen und Größen (Merkmale, Parameter usw.). Ein Bezeichner kann innerhalb einer Klasse nur für eine Größe verwendet werden. Klassennamen werden gemäß verbreiteter Konvention großgeschrieben, andere Bezeichner klein. C++ Frei wählbare Bezeichner bestehen aus alphanumerischen Zeichen und dem Unterstrich. Das erste Zeichen ist nicht numerisch. Klein- und Großschreibung wird unterschieden. Ein Bezeichner kann in einem Programm mehrfach für verschiedene Größen verwendet werden. Namenskonflikte werden durch Sichtbarkeitsregeln gelöst. Java Java zu ergänzen. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Kapitel 9 – Seite 1 von 2 9–2 9 Syntaktische und lexikalische Aspekte C# C# zu ergänzen. 9.4 Wortsymbole Wortsymbole sind von der Sprache reservierte Schlüsselwörter. Component Pascal Wortsymbole und sprachdefinierte Namen werden generell großgeschrieben. Eiffel Bei Wortsymbolen wird nicht zwischen Klein- und Großschreibung unterschieden. Die verbreitete Konvention ist Kleinschreibung. C++ Wortsymbole werden kleingeschrieben. Durch Vorübersetzerdirektiven können ihnen zusätzliche Bezeichner zugeordnet werden. Java Java zu ergänzen. C# C# zu ergänzen. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 A Vorbemerkung Literaturverzeichnis Publikationen zu Informatik- und Softwarethemen erscheinen zahlreich – sowohl Bücher und Zeitschriften als auch Beiträge im Web. Seriöse Verlage wie die hier vertretenen und andere bieten wissenschaftlich, technisch und didaktisch fundierte Werke kompetenter Autoren an. Andererseits hat eine an grellbunten Umschlägen erkennbare Regenbogenpresse der I&KT einen großen Marktanteil erorbert. Kritikpunkte daran sind: Autoren sind oft Nichtfachleute, z.B. Journalisten, die peinliches Halbwissen reproduzieren. Titel orientieren sich vor allem an gängigen Produkten, oft sind es schwache Übersetzungen oder Plagiate von Originalhandbüchern. Die Inhalte sind oft fehlerhaft, schluderig ungenau, unsystematisch, geschwätzig, unnötig redundant. Bei Programmiersprachen werden oft herstellerspezifische Produktdetails als „Standard“ angepriesen. Mit „Tipps und Tricks“ statt wissenschaftlich-ingenieurmäßiger Methodik wendet sich diese Presse eher an Laien und Hacker als an Experten. Zudem ist das Preis/Qualität-Verhältnis fragwürdig. Deshalb stehen solche Publikationen nicht in diesem Literaturverzeichnis. Verlage Grundlagen und Algorithmik Addison-Wesley Addison-Wesley, Boston/San Francisco/New York/Toronto u.a. Addison Wesley Longman Inc., Reading, Massachusetts Addison-Wesley Publishing Company, Reading, Massachusetts Addison-Wesley Pearson Education Inc., Upper Saddle River, NJ Addison-Wesley (Deutschland) GmbH, Bonn Addison-Wesley Verlag, München dpunkt dpunkt.verlag GmbH, Heidelberg Hanser Carl Hanser Verlag, München/Wien Oldenbourg R. Oldenbourg Verlag GmbH, München/Wien Oldenbourg Wissenschaftsverlag GmbH, München Prentice Hall Prentice Hall, Englewood Cliffs Prentice Hall, Upper Saddle River, New Jersey Prentice Hall, München Spektrum Spektrum Akademischer Verlag GmbH, Heidelberg/Berlin Springer Springer-Verlag, Barcelona/Berlin/Budapest/Dordrecht/Heidelberg/ Hong Kong/London/New York/Paris/Tokyo/Wien u.a. Vieweg Friedr. Vieweg & Sohn Verlagsgesellschaft mbH, Braunschweig/ Wiesbaden Friedr. Vieweg & Sohn Verlag / GWV Fachverlage GmbH, Wiesbaden [ApL92] Hans-Jürgen Appelrath, Jochen Ludewig: Skriptum Informatik – eine konventionelle Einführung. Verlag der Fachvereine, Zürich; Teubner, Stuttgart (1992) 2. Aufl., 448 S. [GoZ06] Gerhard Goos, Wolf Zimmermann: Vorlesungen über Informatik. Band 2: Objektorientiertes Programmieren und Algorithmen. Springer (2006) 4. überarbeit. Aufl., 375 S. [HuA04] Peter Hubwieser, Gerd Aiglstorfer: Fundamente der Informatik. Ablaufmodellierung, Algorithmen und Datenstrukturen. Oldenbourg (2004) 276 S. [Lan03] Hans Werner Lang: Algorithmen in Java. Oldenbourg (2003) 261 S. © K. Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1, 27. September 2012 Anhang A – Seite 1 von 10 A–2 A Literaturverzeichnis [Mey09] Bertrand Meyer: Touch of Class. Learning to Program Well with Objects and Contracts. Springer (2009) 876 S. Empfehlenswertes Werk über Grundlagen der objektorientierten Programmierung, richtet sich an Informatikstudenten im Grundstudium. [ScW01] Uwe Schneider, Dieter Werner (Hrsg.): Taschenbuch der Informatik. Fachbuchverlag Leipzig im Carl Hanser Verlag (2001) 4. aktual. Aufl., 876 S. Überblick über 25 Gebiete der Informatik, darunter 52 Seiten über Programmiersprachen (leider mit Ungenauigkeiten und fehlerhaften Beispielalgorithmen). Software-Entwurf und -Entwicklung [GHJV94] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Addison-Wesley (1994) 416 S. Die „Viererbande“ präsentiert hier einen Katalog einfacher und präziser Lösungen für wiederkehrende Entwurfsprobleme der objektorientierten Softwareentwicklung. [GHJV04] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. Addison-Wesley (2004) 504 S. Deutsche Ausgabe von [GHJV94]. [JTM00] Jean-Marc Jézéquel, Michel Train, Christine Mingins: Design Patterns and Contracts. Addison-Wesley (2000) 348 S. Präsentiert die Entwurfsmuster aus [GHJV94] mit vertraglichen Spezifikationen in Eiffel. [Kuc04] Partha Kuchana: Software Architecture Design Patterns in Java. Auerbach Publications, Boca Raton, Florida (2004) 492 S. Präsentiert die Entwurfsmuster aus [GHJV94] und weitere in Java. [LaR06] Bernhard Lahres, Gregor Raýman: Praxisbuch Objektorientierung. Von den Grundlagen zur Umsetzung. Galileo Press, Bonn (2006) 609 S. Spannt den Bogen von Prinzipien der Softwareentwicklung zu Beispielen in C++, Java, C# und JavaScript. [Mey90] Bertrand Meyer: Objektorientierte Softwareentwicklung. Hanser (1990) 547 S. Ein Standardwerk und Bestseller der Objektorientierung; liefert eine softwaretechnisch begründete Einführung in objektorientierte Konzepte und die Programmiersprache Eiffel. [Mey97] Bertrand Meyer: Object-oriented Software Construction. Prentice Hall (1997) 2nd edition, 1254 S. Sehr empfehlenswerte, neue, überarbeitete Auflage der Originalausgabe von [Mey90]. [Oes04] Bernd Oestereich: Objektorientierte Softwareentwicklung. Analyse und Design mit der UML 2.0. Oldenbourg (2004) 6. völlig überarbeit. Aufl., 377 S. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 A–3 A Literaturverzeichnis [ReP99] Peter Rechenberg, Gustav Pomberger (Hrsg.): Informatik-Handbuch. Hanser (1999) 2. aktual. u. erweit. Aufl., 1166 S. Empfehlenswertes Nachschlagewerk mit Beiträgen von 47 Experten zu Themen aus theoretischer, technischer, praktischer, angewandter und Wirtschaftsinformatik sowie zu Daten, Normen und Spezifikationen. Programmiersprachen [BaG94] Henri Bal, Dick Grune: Programming Language Essentials. AddisonWesley (1994) 288 S. [BeG96] Thomas J. Bergin Jr., Richard G. Gibson Jr. (eds.): History of Programming Languages II. ACM Press, New York, Addison-Wesley (1996) 864 S. Konferenzbeiträge zu Algol 68, Pascal, Concurrent Pascal, Ada, Lisp, Prolog, Simulationssprachen, Formac, CLU, Smalltalk, Icon, Forth, C und C++. [BGP00] László Böszörményi, Jürg Gutknecht, Gustav Pomberger (eds): The School of Niklaus Wirth. „The Art of Simplicity“. dpunkt; Morgan Kaufmann Publishers, San Francisco (2000) 260 S. [GoZ99] Gerhard Goos, Wolf Zimmermann: Programmiersprachen. In: [ReP99] S. 469–515 [Gru02] Dominik Gruntz: C# and Java: The Smart Distinctions. In: Journal of Object Technology, Vol. 1, No. 5 (November–December 2002) S. 163– 176, http://www.jot.fm/issues/issue_2002_11/article4 (Zugriff 2010-0304) [HeV07h] Peter A. Henning, Holger Vogelsang (Hrsg.): Handbuch Programmiersprachen. Softwareentwicklung zum Lernen und Nachschlagen. Hanser (2007) 786 S. 23 aktuelle Programmiersprachen in einzelnen Übersichtsbeiträgen. [HeV07t] Peter A. Henning, Holger Vogelsang (Hrsg.): Taschenbuch Programmiersprachen. Fachbuchverlag Leipzig im Carl Hanser Verlag (2007) 2. neu bearbeit. Aufl. 631 S. Kleiner Bruder von [HeV07h], 22 aktuelle Programmiersprachen in einzelnen Übersichtsbeiträgen. [Joy99] Ian Joyner: Objects Unencapsulated. Java, Eiffel, and C++?? Prentice Hall (1999) 386 S. [Kur01] Budi Kurniawan: Comparing C# and Java. O'Reilly Media, Inc. (2001) 13 S., http://ondotnet.com/pub/a/dotnet/2001/06/07/csharp_java.html (Zugriff 2010-03-04) [Lin96] Charles H. Lindsey: A History of Algol 68. In: [BeG96] S. 27–96 [Lou93] Kenneth C. Louden: Programming Languages. Principles and Practice. PWS Publishing Company, Boston (1993) 592 S. Behandelt Geschichtliches, Entwurfsprinzipien, Syntax, Semantik, Datentypen, Ablaufsteuerung, objektorientiertes, funktionales, logisches und nebenläufiges Programmieren und formale Semantikbeschreibungen mit Beispielen in Fortran, Pascal, C, Ada, Modula-2, Smalltalk, C++, Eiffel, ML, Scheme und Prolog. Eine deutsche Ausgabe ist unter dem Titel „Programmiersprachen“ erschienen. [PrZ98] 27.9.12 Terrence Pratt, Marvin Zelkowitz: Programmiersprachen. Design und Implementierung. Prentice Hall (1998) 816 S. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 A–4 A Literaturverzeichnis [Sal98] Peter H. Salus (ed.): Handbook of Programming Languages, Volume I. Object-Oriented Programming Languages. Macmillan Technical Publishing, USA (1998) 944 S. Enthält Beiträge der Sprachentwerfer zu Smalltalk, C++, Eiffel, Ada 95, Modula-3 und Java. [Seb06] Robert W. Sebesta: Concepts of Programming Languages. Pearson Education Inc., Boston (2006) 7th edition, 724 S. Behandelt Geschichtliches, Syntax, Semantik, Namen, Datentypen, Ausdrücke, Ablaufsteuerung, Unterprogramme, objektorientiertes Programmieren, Nebenläufigkeit, Ausnahmebehandlung und funktionale und logische Programmiersprachen mit Beispielen in Fortran, Ada, Smalltalk, C++, Java, C#, JavaScript, PHP, Lisp, ML, Haskell und Prolog. Interessant sind die eingestreuten historischen Anmerkungen und Interviews mit Sprachentwerfern. [Set97] Ravi Sethi: Programming Languages. Concepts and Constructs. AddisonWesley (1997) 2nd edition, 640 S. Behandelt Syntax, Anweisungen, Datentypen, Prozeduren, objektorientiertes, funktionales, logisches und nebenläufiges Programmieren, formale Semantikbeschreibungen und den Lambdakalkül mit Beispielen in Pascal, C, Smalltalk, C++, ML, Scheme, Prolog und Ada. [Sta95] Ryan Stansifer: The Study of Programming Languages. Prentice Hall (1995) 334 S. Eher theoretisch orientiert mit Beispielen aus vielen Sprachen; Geschichtliches, Syntax, Grammatiken, Datentypen, Blöcke, Prozeduren, Module, Prolog, Lambdakalkül, denotationale und axiomatische Semantik. [Wir05] Niklaus Wirth: Good Ideas, Through the Looking Glass. Zürich (2005) 28 S., http://www.cs.inf.ethz.ch/~wirth/Articles/GoodIdeas_origFig.pdf (Zugriff 2010-02-18) [Woo96] Mark Woodman (ed.): Programming Language Choice. Practice and Experience. International Thomson Computer Press, London/Boston (1996) 384 S. [Zep04] Klaus Zeppenfeld: Objektorientierte Programmiersprachen. Einführung und Vergleich von Java, C++, C# und Ruby. Spektrum (2004) 362 S. Stellt die vier Sprachen nacheinander vor, jeweils mit Details beginnend; spricht Konzepte und Sprachvergleiche nur knapp an. Component Pascal [CPLR06] Oberon microsystems, Inc.: Component Pascal Language Report. (October 2006) 32 S., http://www.oberon.ch/pdf/CP-Lang.pdf (Zugriff 201002-18) [Fra00] Michael Franz: Oberon – The Overlooked Jewel. In: [BGP00] S. 41–53 [Hug01] Karlheinz Hug: Module, Klassen, Verträge. Ein Lehrbuch zur komponentenorientierten Softwarekonstruktion. Vieweg (2001) 2. Aufl., 446 S. [Mös94] Hanspeter Mössenböck: Objektorientierte Programmierung in Oberon-2. Springer (1994) 2. Aufl., 286 S. Eine Einführung in objektorientiertes Programmieren; setzt Programmierkenntnisse voraus, enthält Oberon-2-Referenzmanual. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 A–5 A Literaturverzeichnis [ReW94] Martin Reiser, Niklaus Wirth: Programmieren in Oberon. Das neue Pascal. Addison-Wesley (1994) 338 S. Ein einführendes Programmierlehrbuch für Oberon mit einem Kapitel über Oberon-2; gut für Anfänger geeignet. [War02] Eiffel J. Stanley Warford: Computing Fundamentals. The Theory and Practice of Software Design with BlackBox Component Builder. Vieweg (2002) 611 S. [ECMA367] Standard ECMA-367: Eiffel Analysis, Design and Programming Language. (June 2006) 2nd Edition, 174 S., http://www.ecma-international.org/ publications/files/ECMA-ST/ECMA-367.pdf (Zugriff 2012-03-14) Verabschiedete Version des Eiffel-ECMA-Standards der European Computer Manufacturers Association, beruht auf [Mey92]. [ER06] Eiffel: The Reference (Working draft). (1985–2006) http://archive.eiffel.com/nice/language/ (Zugriff 2006-04-07) Die weiter entwickelte Sprachreferenz [Mey92], die dem NICE-Standardisierungskomitee als Vorlage dient, hier als aus FrameMaker generierte HTML-Variante. [Gor96] Jacob Gore: Object Structures. Building Object-Oriented Software Components with Eiffel. Addison-Wesley (1996) 469 S. [Jez96] Jean-Marc Jézéquel: Object-Oriented Software Engineering with Eiffel. Addison Wesley (1996) 340 S. [MaS01] Glenn Maughan, Raphael Simon: Windows Programming Made Easy. Using Object Technology, COM, and the Windows Eiffel Library. Prentice Hall (2001) 726 S. GUI-Programmierung auf Microsoft Windows mit Eiffel. [Mey92] Bertrand Meyer: Eiffel: The Language. Prentice Hall (1992) 594 S. Der Entwerfer von Eiffel diskutiert in diesem umfassenden, kommentierten Referenzmanual auch Ziele, Konzepte und Entwurfsentscheidungen. [Mey95] Bertrand Meyer: Eiffel: The Reference. ISE Technical Report TR-EI-41/ ER (1995) Version 3.3.4, 100 S. Kurzfassung von [Mey92]. 27.9.12 [Mey98] Bertrand Meyer: Eiffel. In: [Sal98] S. 461–551 [Mon93] Frieder Monninger: Eiffel. Objektorientiertes Programmieren in der Praxis. H. Heise Verlag, Hannover (1993) 268 S. [Swi93] Robert Switzer: Eiffel: An Introduction. Prentice Hall (1993) 161 S. [ThW95] Pete Thomas, Ray Weedon: Object-Oriented Programming in Eiffel. Addison-Wesley (1995) 518 S. [TrC01] Jean-Paul Tremblay, Grant A. Cheston: Data Structures and Software Development in an Object-Oriented Domain. Eiffel Edition. Prentice Hall (2001) 1039 S. [Wie95] Richard Wiener: Software Development Using Eiffel. There Can Be Life Other Than C++. Prentice Hall (1995) 425 S. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 A–6 C A Literaturverzeichnis [DaM91] Peter A. Darnell, Philip E. Margolis: C: A Software Engineering Approach. Springer (1991) 2nd edition, 622 S. Meiner Ansicht nach unter den vielen ein gutes C-Buch. [KeR88] Brian W. Kernighan, Dennis M. Ritchie: The C Programming Language, Second Edition, ANSI C. Prentice Hall (1988) Das Standardwerk der Entwerfer von C. [KeR90] Brian W. Kernighan, Dennis M. Ritchie: Programmieren in C. Mit dem CReference-Manual in deutscher Sprache. Hanser (1990) 2. Ausgabe, ANSI C, 283 S. Deutsche Ausgabe von [KeR88]. C++ Über C++ gibt es hunderte von Büchern, viele von der Bauart „Von C nach C++“ mit dem Nachteil, dass sie einem prozeduralen C-Programmierstil verhaftet bleiben und objektorientierte Konzepte und moderne Programmierstile vernachlässigen. Das Folgende beschränkt sich auf einige solide Standardwerke von Autoren mit profunden C++-Kenntnissen, die meist an der Entwicklung der Sprache selbst beteiligt waren oder sind. [BaN94] John J. Barton, Lee R. Nackman: Scientific and Engineering C++: An Introduction with Advanced Techniques and Examples. Addison-Wesley (1994) Für Techniker mit Problemen, deren Lösungen früher mit Fortran programmiert wurden. [Cop92] James O. Coplien: Advanced C++ Programming Styles and Idioms. Addison-Wesley (1992) reprinted with corrections 1994, 520 S. Werk eines C++-Experten für C++-Erfahrene; beginnt, wo [FrK94] endet, mit Datenabstraktion und objektorientierter Programmierung. [C++98] ISO/IEC 14882: International Standard, Programming Languages – C++, Langages de programmation – C++. American National Standards Institute, New York (1998-09-01) First edition, 776 S., http://wwwd0.fnal.gov/~dladams/cxx_standard.pdf (Zugriff 2012-03-06) C++-Standard der International Standards Organization, umfasst auch die Standardbibliothek. [C++11] ISO/IEC 14882: Working Draft, Standard for Programming Language C++. Document Number: N3242=11-0012 (2011-02-28) 1332 S., http:// www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf (Zugriff 2012-03-06) Freie Vorversion des künftigen C++-Standards. [ElS90] Margaret A. Ellis, Bjarne Stroustrup: The Annotated C++ Reference Manual. Addison-Wesley (1990) reprinted with corrections 1995, 470 S. Vollständiges, alle Sprachkonstrukte mit Beispielen kommentierendes Referenzmanual, Basisdokument des ANSI/ISO-C++-Standards, exakteste (nicht mehr aktuelle) Quelle für Experten, als Lehrbuch für Anfänger ungeeignet. [FrK94] Frank L. Friedman, Elliot B. Koffman: Problem Solving, Abstraction, and Design Using C++. Addison-Wesley (1994) 888+ S. Umfangreiche Einführung, dringt aber nur bis zu abstrakten Datentypen vor. Objektorientierte Konzepte, Generizität, Ausnahmebehandlung und andere neue Sprachkonzepte werden nicht behandelt. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 A–7 A Literaturverzeichnis [Jos96] Nicolai Josuttis: Die C++-Standardbibliothek. Eine detaillierte Einführung in die vollständige ANSI/ISO-Schnittstelle. Addison-Wesley (1996) 570 S. [Koe98] Andrew Koenig: C++ Traps and Pitfalls. In: [Sal98] S. 405–458 [KoM00] Andrew Koenig, Barbara E. Moo: Accelerated C++. Practical Programming by Example. Addison-Wesley (2000) 336 S. Beruht auf einem einwöchigen Intensivkurs zu C++, der mit dem Benutzen von Standardbibliotheksklassen startet und dann die Sprachkonzepte behandelt, mit denen die Standardbibliothek implementiert ist. [KoM03] Andrew Koenig, Barbara E. Moo: Intensivkurs C++. Schneller Einstieg über die Standardbibliothek. Pearson Studium, München (2003) 427 S. Deutsche Ausgabe von [KoM00]. [Lip91a] Stanley B. Lippman: C++ Primer. Addison-Wesley (1991) 2nd edition, reprinted with corrections 1995, 614 S. Eine der raren brauchbaren Einführungen eines intimen Kenners von C++; setzt Programmierkenntnisse voraus; beginnt aber erst nach 200 Seiten mit objektorientierter Programmierung; nicht auf Stand des C++-Standards. [Lip91b] Stanley B. Lippman: C++ Einführung und Leitfaden. Addison-Wesley (1991) 2. erweit. Aufl., korrig. Nachdruck 1994, 622 S. Deutsche Ausgabe von [Lip91a]. [Str91] Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley (1991) 2nd edition, reprinted with corrections 1995, 691 S. [Str94a] Bjarne Stroustrup: The Design and Evolution of C++. Addison-Wesley (1994) 461 S. Der Meister schildert, wie und warum sein Werk das wurde, was es ist. Die Diskussion der Ziele, Konzepte und Entwurfsentscheidungen liefert viele Einsichten, die zu besserem Verständnis von C++ beitragen – setzt aber C++-Kenntnisse voraus. [Str94b] Bjarne Stroustrup: Die C++ Programmiersprache. Addison-Wesley (1994) 2. Aufl., 4. korrig. u. erweit. Nachdruck, 717 S. Deutsche Ausgabe der 2. Auflage von [Str97]. [Str94c] Bjarne Stroustrup: Design und Entwicklung von C++. Addison-Wesley (1994) 576S Deutsche Ausgabe von [Str94a]. [Str97] Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley (1997) 3rd edition, 911 S., http://www.ib.cnea.gov.ar/~oop/biblio/ Bjarne_Stroustrup_-_The_C++_Programming_Language_3rd_Ed.pdf (Zugriff 2012-03-06) Das Standardwerk über C++, geschrieben von seinem Entwerfer und ersten Implementierer, setzt Programmierkenntnisse voraus, deckt alle ANSI/ISO-Standard-Sprachkonzepte und die Standardbibliothek ab, enthält ein Referenzmanual. [Str98a] 27.9.12 Bjarne Stroustrup: A History of C++. In: [Sal98] S. 196–303 © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 A–8 A Literaturverzeichnis [Str98b] Bjarne Stroustrup: A Detailed Introduction to C++. In: [Sal98] S. 305– 403 [Str00] Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley (2000) special edition, 1029 S. Gebundene Ausgabe von [Str97] mit zwei zusätzlichen kleinen Anhängen und 1000 kleinen Verbesserungen. Java Auch zu Java gibt es eine umfangreiche Literatur, aus der nur einige Titel genannt sind. [Abt04] Dietmar Abts: Grundkurs JAVA. Von den Grundlagen bis zu Datenbankund Netzanwendungen. Vieweg (2004) 4. verbess. u. erweit. Aufl., 408 S. [AGH05] Ken Arnold, James Gosling, David Holmes: The JavaTM Programming Language, Fourth Edition. Prentice Hall PTR (2005) 928 S. [ArG96] Ken Arnold, James Gosling: The JavaTM Programming Language. Addison-Wesley (1996) [BlN05] Joshua Bloch, Neal Gafter: JavaTM Puzzlers. Traps, Pitfalls, and Corner Cases. Addison-Wesley (2005) 282 S. [Blo01] Joshua Bloch: Effective JavaTM Programming Language Guide. AddisonWesley (2001) 252 S. [Fla97] David Flanagan: Java in a Nutshell: A Desktop Quick Reference. O’Reilly, Sebastopol (1997) [GJS96] James Gosling, Bill Joy, Guy Steele: The JavaTM Language Specification. Addison-Wesley (1996) 825 S. [GJSB00] James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The JavaTM Language Specification, Second Edition. Prentice Hall PTR (2000) 544 S.; Sun Microsystems, Inc.: http://java.sun.com/docs/books/jls/second_edition/html/ j.title.doc.html (Zugriff 2008-11-24) [GJSB05] James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The JavaTM Language Specification, Third Edition. Addison Wesley (2005) 688 S.; Oracle: http:/ /docs.oracle.com/javase/specs/jls/se5.0/jls3.pdf, http://docs.oracle.com/ javase/specs/jls/se5.0/html/j3TOC.html (Zugriff 2012-03-06) [GJSBB11] James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley: The JavaTM Language Specification, Java SE 7 Edition. (July 2011) 670 S.; Oracle: http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf, http://docs.oracle.com/javase/specs/jls/se7/html/index.html (Zugriff 2012-03-06) [Gru05] Ulrich Grude: Java ist eine Sprache. Java lesen, schreiben und ausführen – Eine präzise und verständliche Einführung. Vieweg (2005) 603 S. [HeM05] Steffen Heinzl, Markus Mathes: Middleware in Java. Leitfaden zum Entwurf verteilter Anwendungen – Implementierung von verteilten Systemen über JMS – Verteilte Objekte über RMI und CORBA. Vieweg (2005) 280 S. [KüW05] Wolfgang Küchlin, Andreas Weber: Einführung in die Informatik. Objektorientiert mit Java. Springer (2005) 3. Aufl., 471 S. Grundkonzepte, Sprachkonzepte, Algorithmen, Theorie. [Mös01] Hanspeter Mössenböck: Sprechen Sie Java? Eine Einführung in das systematische Programmieren. dpunkt (2001) 289 S. © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12 A–9 A Literaturverzeichnis C# [Now05] Johannes Nowak: Fortgeschrittene Programmierung mit Java 5. Generics, Annotations, Concurrency und Reflection – mit allen wesentlichen Neuerungen der J2SE 5.0. dpunkt (2005) 266 S. [Pep05] Peter Pepper: Programmieren mit Java. Eine grundlegende Einführung für Informatiker und Ingenieure. Springer (2005) 488 S. Von der auf einige Meter angewachsenen C#-Literatur folgen auch nur wenige Titel. [Arc01] Tom Archer: Inside C#. Objektorientiertes Programmiern der nächsten Generation mit C#. Microsoft Press Deutschland, Unterschleißheim (2001) 351 S. [ECMA334] ECMA International: Standard ECMA-334 C# Language Specification. (June 2006) 4th Edition, 553 S., http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf, Hyperlinked: http://www.jaggersoft.com/csharp_standard/ (Zugriff 2012-03-06) [C#PR12] C# Programmer’s Reference. Visual Studio .NET 2003. (2012) http://msdn.microsoft.com/en-us/library/618ayhy6(v=vs.71).aspx (Zugriff 2012-03-093) [HWG04] Anders Hejlsberg, Scott Wiltamuth, Peter Golde: Die C# Programmiersprache. Die komplette Referenz. Addison-Wesley (2004) 692 S. Deutsche Ausgabe von The C# Programming Language. Weitere Programmiersprachen [Rot04] Heinrich Rottmann: Warum ausgerechnet .NET? Fakten und Vergleiche mit Java und C++ – Beispielprogramme – Glasklare Entscheidungshilfen. Vieweg (2004) 306 S. [WiH04] Scott Wiltamuth, Anders Hejlsberg: C# Language Specification. http:// msdn.microsoft.com/library/default.asp?url=/library/en-us/csspec/html/ CSharpSpecStart.asp (Zugriff 2004-09-23) [D09] Digital Mars: D Programming Language. http://www.digitalmars.com/d (Zugriff 2010-03-05) [WiW66a] Niklaus Wirth, Helmut Weber: EULER: A Generalization of ALGOL, and its Formal Definition: Part I. Comm. ACM, Vol. 9, No. 1 (January 1966) S. 13–25, http://dl.acm.org/citation.cfm?id=365162 (Zugriff 2011-10-06) [WiW66b] Niklaus Wirth, Helmut Weber: EULER: A Generalization of ALGOL, and its Formal Definition: Part II. Comm. ACM, Vol. 9, No. 2 (February 1966) S. 89–99, http://dl.acm.org/citation.cfm?id=365202 (Zugriff 201110-06) Zeitschriften [Go10] The Go Programming Language. http://golang.org (Zugriff 2010-03-04) [Sca10] École Polytechnique Fédérale de Lausanne (EPFL): Scala. Lausanne, Switzerland (2010) http://www.scala-lang.org (Zugriff 2010-03-04) [Spe12] Microsoft Research: Spec#. Microsoft Corporation (2012) http:// research.microsoft.com/en-us/projects/specsharp/ (Zugriff 2012-03-07) Journal of Object-Oriented Programming. Monatlich JavaSPEKTRUM. SIGS-DATACOM GmbH, Troisdorf, zweimonatlich OBJEKTspektrum. SIGS-DATACOM GmbH, Troisdorf, zweimonatlich The C++ Report. The C++ Journal. Vierteljährlich Newsgroups und Bulletin Boards 27.9.12 BIX: c.plus.plus © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 A – 10 A Literaturverzeichnis usenet: Elektronische Quellen comp.lang.c++ Mit Suchmaschinen wie www.google.de findet man spielend zahlreiche Einführungen in Programmiersprachen. [Kin] Bill Kinnersley: The Language List. Collected Information On About 2500 Computer Languages, Past and Present. http://people.ku.edu/~nkinners/ LangList/Extras/langlist.htm (Zugriff 2010-02-18) [Neu99] Michael Neumann: „Hello World“ in 65 verschiedenen Sprachen. Letzte Änderung: 20.11.1999, http://www.ntecs.de/old-hp/s-direktnet/sprachen.htm (Zugriff 2008-06-06) [Neu03] Michael Neumann: 433 Beispiele in 132 (oder 162*) Programmiersprachen. Letzte Änderung: 30.11.2003, http://www.ntecs.de/old-hp/uu9r/ lang/html/lang.de.html (Zugriff 2008-06-06) © Karlheinz Hug, Hochschule Reutlingen, Fak. Informatik, mki-B, I3, Teil 1 27.9.12